diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile index a769a976d..54b5cda0f 100644 --- a/.clusterfuzzlite/Dockerfile +++ b/.clusterfuzzlite/Dockerfile @@ -20,7 +20,7 @@ ENV PIP_ROOT_USER_ACTION=ignore RUN apt-get update && apt-get install --no-install-recommends -y \ cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \ libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \ - libusb-1.0-0-dev libssl-dev pkg-config && \ + libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev && \ apt-get clean && rm -rf /var/lib/apt/lists/* && \ pip install --no-cache-dir -U \ platformio==6.1.16 \ diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..65326bb6d --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix \ No newline at end of file diff --git a/.github/actions/build-variant/action.yml b/.github/actions/build-variant/action.yml index a1e8dd852..c048b7ac2 100644 --- a/.github/actions/build-variant/action.yml +++ b/.github/actions/build-variant/action.yml @@ -76,7 +76,7 @@ runs: done - name: PlatformIO ${{ inputs.arch }} download cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.platformio/.cache key: pio-cache-${{ inputs.arch }}-${{ hashFiles('.github/actions/**', '**.ini') }} @@ -100,7 +100,7 @@ runs: id: version - name: Store binaries as an artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }} overwrite: true diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..14601b058 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,314 @@ +# Meshtastic Firmware - Copilot Instructions + +This document provides context and guidelines for AI assistants working with the Meshtastic firmware codebase. + +## Project Overview + +Meshtastic is an open-source LoRa mesh networking project for long-range, low-power communication without relying on internet or cellular infrastructure. The firmware enables text messaging, location sharing, and telemetry over a decentralized mesh network. + +### Supported Hardware Platforms + +- **ESP32** (ESP32, ESP32-S3, ESP32-C3) - Most common platform +- **nRF52** (nRF52840, nRF52833) - Low power Nordic chips +- **RP2040/RP2350** - Raspberry Pi Pico variants +- **STM32WL** - STM32 with integrated LoRa +- **Linux/Portduino** - Native Linux builds (Raspberry Pi, etc.) + +### Supported Radio Chips + +- **SX1262/SX1268** - Sub-GHz LoRa (868/915 MHz regions) +- **SX1280** - 2.4 GHz LoRa +- **LR1110/LR1120/LR1121** - Wideband radios (sub-GHz and 2.4 GHz capable, but not simultaneously) +- **RF95** - Legacy RFM95 modules +- **LLCC68** - Low-cost LoRa + +### MQTT Integration + +MQTT provides a bridge between Meshtastic mesh networks and the internet, enabling nodes with network connectivity to share messages with remote meshes or external services. + +#### Key Components + +- **`src/mqtt/MQTT.cpp`** - Main MQTT client singleton, handles connection and message routing +- **`src/mqtt/ServiceEnvelope.cpp`** - Protobuf wrapper for mesh packets sent over MQTT +- **`moduleConfig.mqtt`** - MQTT module configuration + +#### MQTT Topic Structure + +Messages are published/subscribed using a hierarchical topic format: + +``` +{root}/{channel_id}/{gateway_id} +``` + +- `root` - Configurable prefix (default: `msh`) +- `channel_id` - Channel name/identifier +- `gateway_id` - Node ID of the publishing gateway + +#### Configuration Defaults (from `Default.h`) + +```cpp +#define default_mqtt_address "mqtt.meshtastic.org" +#define default_mqtt_username "meshdev" +#define default_mqtt_password "large4cats" +#define default_mqtt_root "msh" +#define default_mqtt_encryption_enabled true +#define default_mqtt_tls_enabled false +``` + +#### Key Concepts + +- **Uplink** - Mesh packets sent TO the MQTT broker (controlled by `uplink_enabled` per channel) +- **Downlink** - MQTT messages received and injected INTO the mesh (controlled by `downlink_enabled` per channel) +- **Encryption** - When `encryption_enabled` is true, only encrypted packets are sent; plaintext JSON is disabled +- **ServiceEnvelope** - Protobuf wrapper containing packet + channel_id + gateway_id for routing +- **JSON Support** - Optional JSON encoding for integration with external systems (disabled on nRF52 by default) + +#### PKI Messages + +PKI (Public Key Infrastructure) messages have special handling: + +- Accepted on a special "PKI" channel +- Allow encrypted DMs between nodes that discovered each other on downlink-enabled channels + +## Project Structure + +``` +firmware/ +├── src/ # Main source code +│ ├── main.cpp # Application entry point +│ ├── mesh/ # Core mesh networking +│ │ ├── NodeDB.* # Node database management +│ │ ├── Router.* # Packet routing +│ │ ├── Channels.* # Channel management +│ │ ├── *Interface.* # Radio interface implementations +│ │ └── generated/ # Protobuf generated code +│ ├── modules/ # Feature modules (Position, Telemetry, etc.) +│ ├── gps/ # GPS handling +│ ├── graphics/ # Display drivers and UI +│ ├── platform/ # Platform-specific code +│ ├── input/ # Input device handling +│ └── concurrency/ # Threading utilities +├── variants/ # Hardware variant definitions +│ ├── esp32/ # ESP32 variants +│ ├── esp32s3/ # ESP32-S3 variants +│ ├── nrf52/ # nRF52 variants +│ └── rp2xxx/ # RP2040/RP2350 variants +├── protobufs/ # Protocol buffer definitions +├── boards/ # Custom PlatformIO board definitions +└── bin/ # Build and utility scripts +``` + +## Coding Conventions + +### General Style + +- Follow existing code style - run `trunk fmt` before commits +- Prefer `LOG_DEBUG`, `LOG_INFO`, `LOG_WARN`, `LOG_ERROR` for logging +- Use `assert()` for invariants that should never fail + +### Naming Conventions + +- Classes: `PascalCase` (e.g., `PositionModule`, `NodeDB`) +- Functions/Methods: `camelCase` (e.g., `sendOurPosition`, `getNodeNum`) +- Constants/Defines: `UPPER_SNAKE_CASE` (e.g., `MAX_INTERVAL`, `ONE_DAY`) +- Member variables: `camelCase` (e.g., `lastGpsSend`, `nodeDB`) +- Config defines: `USERPREFS_*` for user-configurable options + +### Key Patterns + +#### Module System + +Modules inherit from `MeshModule` or `ProtobufModule` and implement: + +- `handleReceivedProtobuf()` - Process incoming packets +- `allocReply()` - Generate response packets +- `runOnce()` - Periodic task execution (returns next run interval in ms) + +```cpp +class MyModule : public ProtobufModule +{ + protected: + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_MyMessage *msg) override; + virtual int32_t runOnce() override; +}; +``` + +#### Configuration Access + +- `config.*` - Device configuration (LoRa, position, power, etc.) +- `moduleConfig.*` - Module-specific configuration +- `channels.*` - Channel configuration and management + +#### Default Values + +Use the `Default` class helpers in `src/mesh/Default.h`: + +- `Default::getConfiguredOrDefaultMs(configured, default)` - Returns ms, using default if configured is 0 +- `Default::getConfiguredOrMinimumValue(configured, min)` - Enforces minimum values +- `Default::getConfiguredOrDefaultMsScaled(configured, default, numNodes)` - Scales based on network size + +#### Thread Safety + +- Use `concurrency::Lock` for mutex protection +- Radio SPI access uses `SPILock` +- Prefer `OSThread` for background tasks + +### Hardware Variants + +Each hardware variant has: + +- `variant.h` - Pin definitions and hardware capabilities +- `platformio.ini` - Build configuration +- Optional: `pins_arduino.h`, `rfswitch.h` + +Key defines in variant.h: + +```cpp +#define USE_SX1262 // Radio chip selection +#define HAS_GPS 1 // Hardware capabilities +#define LORA_CS 36 // Pin assignments +#define SX126X_DIO1 14 // Radio-specific pins +``` + +### Protobuf Messages + +- Defined in `protobufs/meshtastic/*.proto` +- Generated code in `src/mesh/generated/` +- Regenerate with `bin/regen-protos.sh` +- Message types prefixed with `meshtastic_` + +### Conditional Compilation + +```cpp +#if !MESHTASTIC_EXCLUDE_GPS // Feature exclusion +#ifdef ARCH_ESP32 // Architecture-specific +#if defined(USE_SX1262) // Radio-specific +#ifdef HAS_SCREEN // Hardware capability +#if USERPREFS_EVENT_MODE // User preferences +``` + +## Build System + +Uses **PlatformIO** with custom scripts: + +- `bin/platformio-pre.py` - Pre-build script +- `bin/platformio-custom.py` - Custom build logic + +Build commands: + +```bash +pio run -e tbeam # Build specific target +pio run -e tbeam -t upload # Build and upload +pio run -e native # Build native/Linux version +``` + +## Common Tasks + +### Adding a New Module + +1. Create `src/modules/MyModule.cpp` and `.h` +2. Inherit from appropriate base class +3. Register in `src/modules/Modules.cpp` +4. Add protobuf messages if needed in `protobufs/` + +### Adding a New Hardware Variant + +1. Create directory under `variants///` +2. Add `variant.h` with pin definitions +3. Add `platformio.ini` with build config +4. Reference common configs with `extends` + +### Modifying Configuration Defaults + +- Check `src/mesh/Default.h` for default value defines +- Check `src/mesh/NodeDB.cpp` for initialization logic +- Consider `isDefaultChannel()` checks for public channel restrictions + +## Important Considerations + +### Traffic Management + +The mesh network has limited bandwidth. When modifying broadcast intervals: + +- Respect minimum intervals on default/public channels +- Use `Default::getConfiguredOrMinimumValue()` to enforce minimums +- Consider `numOnlineNodes` scaling for congestion control + +### Power Management + +Many devices are battery-powered: + +- Use `IF_ROUTER(routerVal, normalVal)` for role-based defaults +- Check `config.power.is_power_saving` for power-saving modes +- Implement proper `sleep()` methods in radio interfaces + +### Channel Security + +- `channels.isDefaultChannel(index)` - Check if using default/public settings +- Default channels get stricter rate limits to prevent abuse +- Private channels may have relaxed limits + +## GitHub Actions CI/CD + +The project uses GitHub Actions extensively for CI/CD. Key workflows are in `.github/workflows/`: + +### Core CI Workflows + +- **`main_matrix.yml`** - Main CI pipeline, runs on push to `master`/`develop` and PRs + - Uses `bin/generate_ci_matrix.py` to dynamically generate build targets + - Builds all supported hardware variants + - PRs build a subset (`--level pr`) for faster feedback + +- **`trunk_check.yml`** - Code quality checks on PRs + - Runs Trunk.io for linting and formatting + - Must pass before merge + +- **`tests.yml`** - End-to-end and hardware tests + - Runs daily on schedule + - Includes native tests and hardware-in-the-loop testing + +- **`test_native.yml`** - Native platform unit tests + - Runs `pio test -e native` + +### Release Workflows + +- **`release_channels.yml`** - Triggered on GitHub release publish + - Builds Docker images + - Packages for PPA (Ubuntu), OBS (openSUSE), and COPR (Fedora) + - Handles Alpha/Beta/Stable release channels + +- **`nightly.yml`** - Nightly builds from develop branch + +- **`docker_build.yml`** / **`docker_manifest.yml`** - Docker image builds + +### Build Matrix Generation + +The CI uses `bin/generate_ci_matrix.py` to dynamically select which targets to build: + +```bash +# Generate full build matrix +./bin/generate_ci_matrix.py all + +# Generate PR-level matrix (subset for faster builds) +./bin/generate_ci_matrix.py all --level pr +``` + +Variants can specify their support level in `platformio.ini`: + +- `custom_meshtastic_support_level = 1` - Actively supported, built on every PR +- `custom_meshtastic_support_level = 2` - Supported, built on merge to main branches +- `board_level = extra` - Extra builds, only on full releases + +### Running Workflows Locally + +Most workflows can be triggered manually via `workflow_dispatch` for testing. + +## Testing + +- Unit tests in `test/` directory +- Run with `pio test -e native` +- Use `bin/test-simulator.sh` for simulation testing + +## Resources + +- [Documentation](https://meshtastic.org/docs/) diff --git a/.github/workflows/build_debian_src.yml b/.github/workflows/build_debian_src.yml index d7d26f0e8..de114be1c 100644 --- a/.github/workflows/build_debian_src.yml +++ b/.github/workflows/build_debian_src.yml @@ -64,7 +64,7 @@ jobs: PKG_VERSION: ${{ steps.version.outputs.deb }} - name: Store binaries as an artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src overwrite: true diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml index 28e4ee994..23690766a 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -21,7 +21,7 @@ jobs: # Use 'arctastic' self-hosted runner pool when building in the main repo runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }} outputs: - artifact-id: ${{ steps.upload.outputs.artifact-id }} + artifact-id: ${{ steps.upload-firmware.outputs.artifact-id }} steps: - uses: actions/checkout@v6 with: @@ -29,23 +29,6 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - name: Set OTA firmware source and target - if: startsWith(inputs.platform, 'esp32') - id: ota_dir - env: - PIO_PLATFORM: ${{ inputs.platform }} - run: | - if [ "$PIO_PLATFORM" = "esp32s3" ]; then - echo "src=firmware-s3.bin" >> $GITHUB_OUTPUT - echo "tgt=release/bleota-s3.bin" >> $GITHUB_OUTPUT - elif [ "$PIO_PLATFORM" = "esp32c3" ] || [ "$PIO_PLATFORM" = "esp32c6" ]; then - echo "src=firmware-c3.bin" >> $GITHUB_OUTPUT - echo "tgt=release/bleota-c3.bin" >> $GITHUB_OUTPUT - elif [ "$PIO_PLATFORM" = "esp32" ]; then - echo "src=firmware.bin" >> $GITHUB_OUTPUT - echo "tgt=release/bleota.bin" >> $GITHUB_OUTPUT - fi - - name: Build ${{ inputs.platform }} id: build uses: meshtastic/gh-action-firmware@main @@ -53,23 +36,83 @@ jobs: pio_platform: ${{ inputs.platform }} pio_env: ${{ inputs.pio_env }} pio_target: build - ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }} - ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }} - - name: Echo manifest from release/firmware-*.mt.json to job summary - if: ${{ always() }} + - name: ESP32 - Download Unified OTA firmware + # Currently only esp32 and esp32s3 use the unified ota + if: inputs.platform == 'esp32' || inputs.platform == 'esp32s3' + id: dl-ota-unified + env: + PIO_PLATFORM: ${{ inputs.platform }} + PIO_ENV: ${{ inputs.pio_env }} + OTA_URL: https://github.com/meshtastic/esp32-unified-ota/releases/latest/download/mt-${{ inputs.platform }}-ota.bin + working-directory: release + run: | + curl -L -o "mt-$PIO_PLATFORM-ota.bin" $OTA_URL + + - name: ESP32-C* - Download BLE-Only OTA firmware + if: inputs.platform == 'esp32c3' || inputs.platform == 'esp32c6' + id: dl-ota-ble + env: + PIO_ENV: ${{ inputs.pio_env }} + OTA_URL: https://github.com/meshtastic/firmware-ota/releases/latest/download/firmware-c3.bin + working-directory: release + run: | + curl -L -o bleota-c3.bin $OTA_URL + + - name: Update manifest with OTA file + if: inputs.platform == 'esp32' || inputs.platform == 'esp32s3' || inputs.platform == 'esp32c3' || inputs.platform == 'esp32c6' + working-directory: release + env: + PIO_PLATFORM: ${{ inputs.platform }} + run: | + # Determine OTA filename based on platform + if [[ "$PIO_PLATFORM" == "esp32" || "$PIO_PLATFORM" == "esp32s3" ]]; then + OTA_FILE="mt-${PIO_PLATFORM}-ota.bin" + else + OTA_FILE="bleota-c3.bin" + fi + + # Check if OTA file exists + if [[ ! -f "$OTA_FILE" ]]; then + echo "OTA file $OTA_FILE not found, skipping manifest update" + exit 0 + fi + + # Calculate MD5 and size + if command -v md5sum &> /dev/null; then + OTA_MD5=$(md5sum "$OTA_FILE" | cut -d' ' -f1) + else + OTA_MD5=$(md5 -q "$OTA_FILE") + fi + OTA_SIZE=$(stat -f%z "$OTA_FILE" 2>/dev/null || stat -c%s "$OTA_FILE") + + # Find and update manifest file + for manifest in firmware-*.mt.json; do + if [[ -f "$manifest" ]]; then + echo "Updating $manifest with $OTA_FILE (md5: $OTA_MD5, size: $OTA_SIZE)" + # Add OTA entry to files array if not already present + jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" --arg part "app1" \ + 'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes, "part_name": $part}] else . end' \ + "$manifest" > "${manifest}.tmp" && mv "${manifest}.tmp" "$manifest" + fi + done + + - name: Job summary env: PIO_ENV: ${{ inputs.pio_env }} run: | - echo "## Manifest: \`$PIO_ENV\`" >> $GITHUB_STEP_SUMMARY + echo "## $PIO_ENV" >> $GITHUB_STEP_SUMMARY + echo "
Manifest" >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY echo '```json' >> $GITHUB_STEP_SUMMARY cat release/firmware-*.mt.json >> $GITHUB_STEP_SUMMARY echo '' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY - name: Store binaries as an artifact - uses: actions/upload-artifact@v5 - id: upload + uses: actions/upload-artifact@v6 + id: upload-firmware with: name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }} overwrite: true @@ -82,3 +125,12 @@ jobs: release/*.zip release/device-*.sh release/device-*.bat + + - name: Store manifests as an artifact + uses: actions/upload-artifact@v6 + id: upload-manifest + with: + name: manifest-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }} + overwrite: true + path: | + release/*.mt.json diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml index 9d9e0114b..9cc0bac78 100644 --- a/.github/workflows/build_one_target.yml +++ b/.github/workflows/build_one_target.yml @@ -98,7 +98,7 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: path: ./ pattern: firmware-*-* @@ -111,7 +111,7 @@ jobs: run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat - name: Repackage in single firmware zip - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }} overwrite: true @@ -127,7 +127,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: pattern: firmware-*-${{ needs.version.outputs.long }} merge-multiple: true @@ -146,7 +146,7 @@ jobs: run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output - name: Repackage in single elfs zip - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip overwrite: true diff --git a/.github/workflows/first_time_contributor.yml b/.github/workflows/first_time_contributor.yml new file mode 100644 index 000000000..1ebc9a602 --- /dev/null +++ b/.github/workflows/first_time_contributor.yml @@ -0,0 +1,47 @@ +name: Welcome First-Time Contributor + +on: + issues: + types: opened + pull_request_target: + types: opened + +permissions: {} + +jobs: + welcome: + runs-on: ubuntu-latest + permissions: + issues: write # Required to post comments and labels on issues + pull-requests: write # Required to post comments and labels on PRs + steps: + - uses: plbstl/first-contribution@v4 + with: + labels: first-contribution + issue-opened-msg: | + ### @{fc-author}, Welcome to Meshtastic! :wave: + + Thanks for opening your first issue. If it's helpful, an easy way + to get logs is the "Open Serial Monitor" button on the [Web Flasher](https://flasher.meshtastic.org). + + If you have ideas for features, note that we often debate big ideas + in the [discussions tab](https://github.com/meshtastic/firmware/discussions/categories/ideas) + first. This tracker tends to be for ideas that have community + consensus and a clear implementation. + + We're very active [on discord](https://discord.com/invite/meshtastic), + especially the \#firmware and \#alphanauts-testing channels. If you'll + be around for a while, we'd love to see you there! + + Welcome to the community! :heart: + + pr-opened-msg: | + ### @{fc-author}, Welcome to Meshtastic! + + Thanks for opening your first pull request. We really appreciate it. + + We discuss work as a team in discord, please join us in the [#firmware channel](https://discord.com/invite/meshtastic). + There's a big backlog of patches at the moment. If you have time, + please help us with some code review and testing of [other PRs](https://github.com/meshtastic/firmware/pulls)! + + Welcome to the team :smile: diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index acd63f28f..6b48e8128 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -8,7 +8,9 @@ on: branches: - master - develop + - pioarduino # Remove when merged // use `feature/` in the future. - event/* + - feature/* paths-ignore: - "**.md" - version.properties @@ -18,7 +20,9 @@ on: branches: - master - develop + - pioarduino # Remove when merged // use `feature/` in the future. - event/* + - feature/* paths-ignore: - "**.md" #- "**.yml" @@ -77,16 +81,21 @@ jobs: fail-fast: false matrix: check: ${{ fromJson(needs.setup.outputs.check) }} - - runs-on: ubuntu-latest + # Use 'arctastic' self-hosted runner pool when checking in the main repo + runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }} if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} steps: - uses: actions/checkout@v6 - - name: Build base - id: base - uses: ./.github/actions/setup-base + with: + submodules: recursive + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} - name: Check ${{ matrix.check.board }} - run: bin/check-all.sh ${{ matrix.check.board }} + uses: meshtastic/gh-action-firmware@main + with: + pio_platform: ${{ matrix.check.platform }} + pio_env: ${{ matrix.check.board }} + pio_target: check build: needs: [setup, version] @@ -168,7 +177,7 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: path: ./ pattern: firmware-${{matrix.arch}}-* @@ -178,7 +187,7 @@ jobs: run: ls -R - name: Repackage in single firmware zip - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true @@ -192,10 +201,11 @@ jobs: ./device-*.bat ./littlefs-*.bin ./bleota*bin + ./mt-*-ota.bin ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -214,7 +224,7 @@ jobs: run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - name: Repackage in single elfs zip - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true @@ -228,6 +238,48 @@ jobs: description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation" github-token: ${{ secrets.GITHUB_TOKEN }} + shame: + if: github.repository == 'meshtastic/firmware' + continue-on-error: true + runs-on: ubuntu-latest + needs: [build] + steps: + - uses: actions/checkout@v6 + if: github.event_name == 'pull_request_target' + with: + filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head) + fetch-depth: 0 + - name: Download the current manifests + uses: actions/download-artifact@v7 + with: + path: ./manifests-new/ + pattern: manifest-* + merge-multiple: true + - name: Upload combined manifests for later commit and global stats crunching. + uses: actions/upload-artifact@v6 + id: upload-manifest + with: + name: manifests-${{ github.sha }} + overwrite: true + path: manifests-new/*.mt.json + - name: Find the merge base + if: github.event_name == 'pull_request_target' + run: echo "MERGE_BASE=$(git merge-base "origin/$base" "$head")" >> $GITHUB_ENV + env: + base: ${{ github.base_ref }} + head: ${{ github.sha }} + # Currently broken (for-loop through EVERY artifact -- rate limiting) + # - name: Download the old manifests + # if: github.event_name == 'pull_request_target' + # run: gh run download -R "$repo" --name "manifests-$merge_base" --dir manifest-old/ + # env: + # GH_TOKEN: ${{ github.token }} + # merge_base: ${{ env.MERGE_BASE }} + # repo: ${{ github.repository }} + # - name: Do scan and post comment + # if: github.event_name == 'pull_request_target' + # run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/ + release-artifacts: runs-on: ubuntu-latest if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} @@ -242,6 +294,24 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: 3.x + + - name: Generate release notes + id: release_notes + run: | + chmod +x ./bin/generate_release_notes.py + NOTES=$(./bin/generate_release_notes.py ${{ needs.version.outputs.long }}) + echo "notes<> $GITHUB_OUTPUT + echo "$NOTES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create release uses: softprops/action-gh-release@v2 @@ -251,18 +321,17 @@ jobs: prerelease: true name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha tag_name: v${{ needs.version.outputs.long }} - body: | - Autogenerated by github action, developer should edit as required before publishing... + body: ${{ steps.release_notes.outputs.notes }} - name: Download source deb - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src merge-multiple: true path: ./output/debian-src - name: Download `native-tft` pio deps - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true @@ -286,7 +355,7 @@ jobs: }' > firmware-${{ needs.version.outputs.long }}.json - name: Save Release manifest artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: manifest-${{ needs.version.outputs.long }} overwrite: true @@ -327,7 +396,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -344,7 +413,7 @@ jobs: - name: Zip firmware run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -376,6 +445,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v6 @@ -383,18 +454,25 @@ jobs: python-version: 3.x - name: Get firmware artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} merge-multiple: true path: ./publish - name: Get manifest artifact - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: pattern: manifest-${{ needs.version.outputs.long }} path: ./publish + - name: Generate release notes + run: | + chmod +x ./bin/generate_release_notes.py + ./bin/generate_release_notes.py ${{ needs.version.outputs.long }} > ./publish/release_notes.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Publish firmware to meshtastic.github.io uses: peaceiris/actions-gh-pages@v4 env: diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml index a71afad9d..bd3f6d4eb 100644 --- a/.github/workflows/merge_queue.yml +++ b/.github/workflows/merge_queue.yml @@ -147,7 +147,7 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: path: ./ pattern: firmware-${{matrix.arch}}-* @@ -160,7 +160,7 @@ jobs: run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat - name: Repackage in single firmware zip - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true @@ -176,7 +176,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -195,7 +195,7 @@ jobs: run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - name: Repackage in single elfs zip - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true @@ -235,14 +235,14 @@ jobs: Autogenerated by github action, developer should edit as required before publishing... - name: Download source deb - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src merge-multiple: true path: ./output/debian-src - name: Download `native-tft` pio deps - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true @@ -292,7 +292,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -309,7 +309,7 @@ jobs: - name: Zip firmware run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -347,7 +347,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} merge-multiple: true diff --git a/.github/workflows/models_issue_triage.yml b/.github/workflows/models_issue_triage.yml new file mode 100644 index 000000000..ef12885f8 --- /dev/null +++ b/.github/workflows/models_issue_triage.yml @@ -0,0 +1,213 @@ +name: Issue Triage (Models) + +on: + issues: + types: [opened] + +permissions: + issues: write + models: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: true + +jobs: + triage: + if: ${{ github.repository == 'meshtastic/firmware' && github.event.issue.user.type != 'Bot' }} + runs-on: ubuntu-latest + steps: + # ───────────────────────────────────────────────────────────────────────── + # Step 1: Quality check (spam/AI-slop detection) - runs first, exits early if spam + # ───────────────────────────────────────────────────────────────────────── + - name: Detect spam or low-quality content + uses: actions/ai-inference@v2 + id: quality + continue-on-error: true + with: + max-tokens: 20 + prompt: | + Is this GitHub issue spam, AI-generated slop, or low quality? + + Title: ${{ github.event.issue.title }} + Body: ${{ github.event.issue.body }} + + Respond with exactly one of: spam, ai-generated, needs-review, ok + system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop. + model: openai/gpt-4o-mini + + - name: Apply quality label if needed + if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok' + uses: actions/github-script@v8 + env: + QUALITY_LABEL: ${{ steps.quality.outputs.response }} + with: + script: | + const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase(); + const labelMeta = { + 'spam': { color: 'd73a4a', description: 'Possible spam' }, + 'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' }, + 'needs-review': { color: 'f9d0c4', description: 'Needs human review' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] }); + + // Set output to skip remaining steps + core.setOutput('is_spam', 'true'); + + # ───────────────────────────────────────────────────────────────────────── + # Step 2: Duplicate detection - only if not spam + # ───────────────────────────────────────────────────────────────────────── + - name: Detect duplicate issues + if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '' + uses: pelikhan/action-genai-issue-dedup@bdb3b5d9451c1090ffcdf123d7447a5e7c7a2528 # v0.0.19 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + # ───────────────────────────────────────────────────────────────────────── + # Step 3: Completeness check + auto-labeling (combined into one AI call) + # ───────────────────────────────────────────────────────────────────────── + - name: Determine if completeness check should be skipped + if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '' + uses: actions/github-script@v8 + id: check-skip + with: + script: | + const title = (context.payload.issue.title || '').toLowerCase(); + const labels = (context.payload.issue.labels || []).map(label => label.name); + const hasFeatureRequest = title.includes('feature request'); + const hasEnhancement = labels.includes('enhancement'); + const shouldSkip = hasFeatureRequest && hasEnhancement; + core.setOutput('should_skip', shouldSkip ? 'true' : 'false'); + + - name: Analyze issue completeness and determine labels + if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' + uses: actions/ai-inference@v2 + id: analysis + continue-on-error: true + with: + prompt: | + Analyze this GitHub issue for completeness and determine if it needs labels. + + IMPORTANT: Distinguish between: + - Device/firmware bugs (crashes, reboots, lockups, radio/GPS/display/power issues) - these need device logs + - Build/release/packaging issues (missing files, CI failures, download problems) - these do NOT need device logs + - Documentation or website issues - these do NOT need device logs + + If this is a device/firmware bug, request device logs and explain how to get them: + + Web Flasher logs: + - Go to https://flasher.meshtastic.org + - Connect the device via USB and click Connect + - Open the device console/log output, reproduce the problem, then copy/download and attach/paste the logs + + Meshtastic CLI logs: + - Run: meshtastic --port --noproto + - Reproduce the problem, then copy/paste the terminal output + + Also request key context if missing: device model/variant, firmware version, region, steps to reproduce, expected vs actual. + + Respond ONLY with valid JSON (no markdown, no code fences): + {"complete": true, "comment": "", "label": "none"} + OR + {"complete": false, "comment": "Your helpful comment", "label": "needs-logs"} + + Use "needs-logs" ONLY if this is a device/firmware bug AND no logs are attached. + Use "needs-info" if basic info like firmware version or steps to reproduce are missing. + Use "none" if the issue is complete, is a feature request, or is a build/CI/packaging issue. + + Title: ${{ github.event.issue.title }} + Body: ${{ github.event.issue.body }} + system-prompt: You are a helpful assistant that triages GitHub issues. Be conservative with labels. Only request device logs for actual device/firmware bugs, not for build/release/CI issues. + model: openai/gpt-4o-mini + + - name: Process analysis result + if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != '' + uses: actions/github-script@v8 + id: process + env: + AI_RESPONSE: ${{ steps.analysis.outputs.response }} + with: + script: | + let raw = (process.env.AI_RESPONSE || '').trim(); + + // Strip markdown code fences if present + raw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim(); + + let complete = true; + let comment = ''; + let label = 'none'; + + try { + const parsed = JSON.parse(raw); + complete = !!parsed.complete; + comment = (parsed.comment ?? '').toString().trim(); + label = (parsed.label ?? 'none').toString().trim().toLowerCase(); + } catch { + // If JSON parse fails, log warning and don't comment (avoid posting raw JSON) + console.log('Failed to parse AI response as JSON:', raw); + complete = true; + comment = ''; + label = 'none'; + } + + // Validate label + const allowedLabels = new Set(['needs-logs', 'needs-info', 'none']); + if (!allowedLabels.has(label)) label = 'none'; + + // Only comment if we have a valid parsed comment (not raw JSON) + const shouldComment = !complete && comment.length > 0 && !comment.startsWith('{'); + core.setOutput('should_comment', shouldComment ? 'true' : 'false'); + core.setOutput('comment_body', comment); + core.setOutput('label', label); + + - name: Apply triage label + if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none' + uses: actions/github-script@v8 + env: + LABEL_NAME: ${{ steps.process.outputs.label }} + with: + script: | + const label = process.env.LABEL_NAME; + const labelMeta = { + 'needs-logs': { color: 'cfd3d7', description: 'Device logs requested for triage' }, + 'needs-info': { color: 'f9d0c4', description: 'More information requested for triage' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] }); + + - name: Comment on issue + if: steps.process.outputs.should_comment == 'true' + uses: actions/github-script@v8 + env: + COMMENT_BODY: ${{ steps.process.outputs.comment_body }} + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: process.env.COMMENT_BODY + }); diff --git a/.github/workflows/models_pr_triage.yml b/.github/workflows/models_pr_triage.yml new file mode 100644 index 000000000..d4c8509d2 --- /dev/null +++ b/.github/workflows/models_pr_triage.yml @@ -0,0 +1,139 @@ +name: PR Triage (Models) + +on: + pull_request_target: + types: [opened] + +permissions: + pull-requests: write + issues: write + models: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + triage: + if: ${{ github.repository == 'meshtastic/firmware' && github.event.pull_request.user.type != 'Bot' }} + runs-on: ubuntu-latest + steps: + # ───────────────────────────────────────────────────────────────────────── + # Step 1: Check if PR already has automation/type labels (skip if so) + # ───────────────────────────────────────────────────────────────────────── + - name: Check existing labels + uses: actions/github-script@v8 + id: check-labels + with: + script: | + const skipLabels = new Set(['automation']); + const typeLabels = new Set(['bugfix', 'hardware-support', 'enhancement', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']); + const prLabels = context.payload.pull_request.labels.map(l => l.name); + + const shouldSkipAll = prLabels.some(l => skipLabels.has(l)); + const hasTypeLabel = prLabels.some(l => typeLabels.has(l)); + + core.setOutput('skip_all', shouldSkipAll ? 'true' : 'false'); + core.setOutput('has_type_label', hasTypeLabel ? 'true' : 'false'); + + # ───────────────────────────────────────────────────────────────────────── + # Step 2: Quality check (spam/AI-slop detection) + # ───────────────────────────────────────────────────────────────────────── + - name: Detect spam or low-quality content + if: steps.check-labels.outputs.skip_all != 'true' + uses: actions/ai-inference@v2 + id: quality + continue-on-error: true + with: + max-tokens: 20 + prompt: | + Is this GitHub pull request spam, AI-generated slop, or low quality? + + Title: ${{ github.event.pull_request.title }} + Body: ${{ github.event.pull_request.body }} + + Respond with exactly one of: spam, ai-generated, needs-review, ok + system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop. + model: openai/gpt-4o-mini + + - name: Apply quality label if needed + if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok' + uses: actions/github-script@v8 + id: quality-label + env: + QUALITY_LABEL: ${{ steps.quality.outputs.response }} + with: + script: | + const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase(); + const labelMeta = { + 'spam': { color: 'd73a4a', description: 'Possible spam' }, + 'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' }, + 'needs-review': { color: 'f9d0c4', description: 'Needs human review' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] }); + + core.setOutput('is_spam', 'true'); + + # ───────────────────────────────────────────────────────────────────────── + # Step 3: Auto-label PR type (bugfix/hardware-support/enhancement) + # Only skip for spam/ai-generated; still classify needs-review PRs + # ───────────────────────────────────────────────────────────────────────── + - name: Classify PR for labeling + if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.quality.outputs.response != 'spam' && steps.quality.outputs.response != 'ai-generated' + uses: actions/ai-inference@v2 + id: classify + continue-on-error: true + with: + max-tokens: 30 + prompt: | + Classify this pull request into exactly one category. + + Return exactly one of: bugfix, hardware-support, enhancement + + Use bugfix if it fixes a bug, crash, or incorrect behavior. + Use hardware-support if it adds or improves support for a specific hardware device/variant. + Use enhancement if it adds a new feature, improves performance, or refactors code. + + Title: ${{ github.event.pull_request.title }} + Body: ${{ github.event.pull_request.body }} + system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label. + model: openai/gpt-4o-mini + + - name: Apply type label + if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != '' + uses: actions/github-script@v8 + env: + TYPE_LABEL: ${{ steps.classify.outputs.response }} + with: + script: | + const label = (process.env.TYPE_LABEL || '').trim().toLowerCase(); + const labelMeta = { + 'bugfix': { color: 'd73a4a', description: 'Bug fix' }, + 'hardware-support': { color: '0e8a16', description: 'Hardware support addition or improvement' }, + 'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] }); diff --git a/.github/workflows/package_obs.yml b/.github/workflows/package_obs.yml index 2b202ed95..63f1fe8a0 100644 --- a/.github/workflows/package_obs.yml +++ b/.github/workflows/package_obs.yml @@ -58,7 +58,7 @@ jobs: id: version - name: Download artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src merge-multiple: true diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml index cb10a79f3..82ffe66e9 100644 --- a/.github/workflows/package_pio_deps.yml +++ b/.github/workflows/package_pio_deps.yml @@ -56,7 +56,7 @@ jobs: PLATFORMIO_CORE_DIR: pio/core - name: Store binaries as an artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }} overwrite: true diff --git a/.github/workflows/package_ppa.yml b/.github/workflows/package_ppa.yml index 2e3278041..9a463dbea 100644 --- a/.github/workflows/package_ppa.yml +++ b/.github/workflows/package_ppa.yml @@ -60,7 +60,7 @@ jobs: id: version - name: Download artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src merge-multiple: true diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml index a3e0b23cf..6306d777f 100644 --- a/.github/workflows/pr_tests.yml +++ b/.github/workflows/pr_tests.yml @@ -50,7 +50,7 @@ jobs: - name: Download test artifacts if: needs.native-tests.result != 'skipped' - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: platformio-test-report-${{ steps.version.outputs.long }} merge-multiple: true diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index f21b13ee1..7f925b67c 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -48,6 +48,37 @@ jobs: ${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }} secrets: inherit + publish-release-notes: + if: github.event.action == 'published' + runs-on: ubuntu-latest + steps: + - name: Get release version + id: version + run: | + # Extract version from tag (e.g., v2.7.15.567b8ea -> 2.7.15.567b8ea) + VERSION=${GITHUB_REF#refs/tags/v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Get release notes + run: | + mkdir -p ./publish + gh release view ${{ github.event.release.tag_name }} --json body --jq '.body' > ./publish/release_notes.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish release notes to meshtastic.github.io + uses: peaceiris/actions-gh-pages@v4 + with: + deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }} + external_repository: meshtastic/meshtastic.github.io + publish_branch: master + publish_dir: ./publish + destination_dir: firmware-${{ steps.version.outputs.version }} + user_name: github-actions[bot] + user_email: github-actions[bot]@users.noreply.github.com + commit_message: Release notes for ${{ steps.version.outputs.version }} + enable_jekyll: true + # Create a PR to bump version when a release is Published bump-version: if: github.event.action == 'published' @@ -102,7 +133,7 @@ jobs: PIP_DISABLE_PIP_VERSION_CHECK: 1 - name: Create Bumps pull request - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@v8 with: base: ${{ github.event.repository.default_branch }} branch: create-pull-request/bump-version diff --git a/.github/workflows/sec_sast_semgrep_cron.yml b/.github/workflows/sec_sast_semgrep_cron.yml index d044f9038..d93449d6d 100644 --- a/.github/workflows/sec_sast_semgrep_cron.yml +++ b/.github/workflows/sec_sast_semgrep_cron.yml @@ -33,7 +33,7 @@ jobs: # step 3 - name: save report as pipeline artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: report.sarif overwrite: true diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml index 26ff306a9..b527c2fd9 100644 --- a/.github/workflows/test_native.yml +++ b/.github/workflows/test_native.yml @@ -59,7 +59,7 @@ jobs: id: version - name: Save coverage information - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() # run this step even if previous step failed with: name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }} @@ -94,7 +94,7 @@ jobs: - name: Save test results if: always() # run this step even if previous step failed - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: platformio-test-report-${{ steps.version.outputs.long }} overwrite: true @@ -108,7 +108,7 @@ jobs: sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative. - name: Save coverage information - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() # run this step even if previous step failed with: name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }} @@ -137,20 +137,20 @@ jobs: id: version - name: Download test artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: platformio-test-report-${{ steps.version.outputs.long }} merge-multiple: true - name: Test Report - uses: dorny/test-reporter@v2.3.0 + uses: dorny/test-reporter@v2.5.0 with: name: PlatformIO Tests path: testreport.xml reporter: java-junit - name: Download coverage artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }} path: code-coverage-report @@ -163,7 +163,7 @@ jobs: genhtml --quiet --legend --prefix "${PWD}" code-coverage-report/coverage_src.info --output-directory code-coverage-report - name: Save Code Coverage Report - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: code-coverage-report-${{ steps.version.outputs.long }} path: code-coverage-report diff --git a/.github/workflows/trunk_format_pr.yml b/.github/workflows/trunk_format_pr.yml deleted file mode 100644 index 8fa0cc1eb..000000000 --- a/.github/workflows/trunk_format_pr.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Run Trunk Fmt on PR Comment - -on: - issue_comment: - types: [created] - -permissions: read-all - -jobs: - trunk-fmt: - if: github.event.issue.pull_request != null && contains(github.event.comment.body, 'trunk fmt') - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Install trunk - run: curl https://get.trunk.io -fsSL | bash - - - name: Run Trunk Fmt - run: trunk fmt - - - name: Get release version string - run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT - id: version - - - name: Commit and push changes - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git add . - git commit -m "Add firmware version ${{ steps.version.outputs.long }}" - git push - - - name: Comment on PR - uses: actions/github-script@v8 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: '`trunk fmt` has been run on this PR.' - }) diff --git a/.github/workflows/update_protobufs.yml b/.github/workflows/update_protobufs.yml index af0557fda..35565d1e4 100644 --- a/.github/workflows/update_protobufs.yml +++ b/.github/workflows/update_protobufs.yml @@ -16,7 +16,7 @@ jobs: submodules: true - name: Update submodule - if: ${{ github.ref == 'refs/heads/master' }} + if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' }} run: | git submodule update --remote protobufs @@ -31,7 +31,7 @@ jobs: ./bin/regen-protos.sh - name: Create pull request - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@v8 with: branch: create-pull-request/update-protobufs labels: submodules diff --git a/.gitignore b/.gitignore index cc742c6c1..d6d97c6c4 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,15 @@ src/mesh/raspihttp/private_key.pem # Ignore logo (set at build time with platformio-custom.py) data/boot/logo.* + +# pioarduino platform +managed_components/* +arduino-lib-builder* +dependencies.lock +idf_component.yml +CMakeLists.txt +/sdkconfig.* +.dummy/* + +# PYTHONPATH used by the Nix shell +.python3 diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 95e5b0dd2..9d563a39a 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -8,25 +8,25 @@ plugins: uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.495 - - renovate@42.30.4 - - prettier@3.7.4 - - trufflehog@3.91.2 - - yamllint@1.37.1 - - bandit@1.9.2 - - trivy@0.67.2 + - checkov@3.2.497 + - renovate@42.84.2 + - prettier@3.8.0 + - trufflehog@3.92.5 + - yamllint@1.38.0 + - bandit@1.9.3 + - trivy@0.68.2 - taplo@0.10.0 - - ruff@0.14.7 + - ruff@0.14.13 - isort@7.0.0 - - markdownlint@0.46.0 - - oxipng@9.1.5 + - markdownlint@0.47.0 + - oxipng@10.0.0 - svgo@4.0.0 - - actionlint@1.7.9 + - actionlint@1.7.10 - flake8@7.3.0 - hadolint@2.14.0 - shfmt@3.6.0 - shellcheck@0.11.0 - - black@25.11.0 + - black@26.1.0 - git-diff-check - gitleaks@8.30.0 - clang-format@16.0.3 diff --git a/Dockerfile b/Dockerfile index 111dd69fc..91d3f7796 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ curl wget g++ zip git ca-certificates pkg-config \ libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \ libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \ - libx11-dev libinput-dev libxkbcommon-x11-dev \ + libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev \ && apt-get clean && rm -rf /var/lib/apt/lists/* \ && pip install --no-cache-dir -U platformio \ && mkdir /tmp/firmware diff --git a/SECURITY.md b/SECURITY.md index 5092595e1..a77a4d028 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,8 @@ | Firmware Version | Supported | | ---------------- | ------------------ | -| 2.6.x | :white_check_mark: | -| <= 2.5.x | :x: | +| 2.7.x | :white_check_mark: | +| <= 2.6.x | :x: | ## Reporting a Vulnerability diff --git a/alpine.Dockerfile b/alpine.Dockerfile index b3b384101..64c281788 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -11,7 +11,7 @@ RUN apk --no-cache add \ bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \ libgpiod-dev yaml-cpp-dev bluez-dev \ libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \ - libx11-dev libinput-dev libxkbcommon-dev \ + libx11-dev libinput-dev libxkbcommon-dev sqlite-dev \ && rm -rf /var/cache/apk/* \ && pip install --no-cache-dir -U platformio \ && mkdir /tmp/firmware diff --git a/bin/build-esp32.sh b/bin/build-esp32.sh index 8c684aa7e..d07a09a16 100755 --- a/bin/build-esp32.sh +++ b/bin/build-esp32.sh @@ -22,7 +22,7 @@ export APP_VERSION=$VERSION basename=firmware-$1-$VERSION -pio run --environment $1 # -v +pio run --environment $1 -t mtjson # -v cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf @@ -32,20 +32,10 @@ cp $BUILDDIR/$basename.factory.bin $OUTDIR/$basename.factory.bin echo "Copying ESP32 update bin file" cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin -echo "Building Filesystem for ESP32 targets" -# If you want to build the webui, uncomment the following lines -# pio run --environment $1 -t buildfs -# cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin -# # Remove webserver files from the filesystem and rebuild -# ls -l data/static # Diagnostic list of files -# rm -rf data/static -pio run --environment $1 -t buildfs --disable-auto-clean +echo "Copying Filesystem for ESP32 targets" cp $BUILDDIR/littlefs-$1-$VERSION.bin $OUTDIR/littlefs-$1-$VERSION.bin cp bin/device-install.* $OUTDIR/ cp bin/device-update.* $OUTDIR/ -# Generate the manifest file -echo "Generating Meshtastic manifest" -TIMEFORMAT="Generated manifest in %E seconds" -time pio run --environment $1 -t mtjson --silent --disable-auto-clean -cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json +echo "Copying manifest" +cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true diff --git a/bin/build-nrf52.sh b/bin/build-nrf52.sh index c605fb1e0..99187ba0d 100755 --- a/bin/build-nrf52.sh +++ b/bin/build-nrf52.sh @@ -21,13 +21,14 @@ rm -f $BUILDDIR/firmware* export APP_VERSION=$VERSION basename=firmware-$1-$VERSION +ota_basename=${basename}-ota -pio run --environment $1 # -v +pio run --environment $1 -t mtjson # -v cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf echo "Copying NRF52 dfu (OTA) file" -cp $BUILDDIR/$basename.zip $OUTDIR/$basename.zip +cp $BUILDDIR/$basename.zip $OUTDIR/$ota_basename.zip echo "Copying NRF52 UF2 file" cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2 @@ -47,8 +48,5 @@ if (echo $1 | grep -q "rak4631"); then cp $SRCHEX $OUTDIR/ fi -# Generate the manifest file -echo "Generating Meshtastic manifest" -TIMEFORMAT="Generated manifest in %E seconds" -time pio run --environment $1 -t mtjson --silent --disable-auto-clean -cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json +echo "Copying manifest" +cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true diff --git a/bin/build-rp2xx0.sh b/bin/build-rp2xx0.sh index ae26fdfbf..992a39be7 100755 --- a/bin/build-rp2xx0.sh +++ b/bin/build-rp2xx0.sh @@ -22,15 +22,12 @@ export APP_VERSION=$VERSION basename=firmware-$1-$VERSION -pio run --environment $1 # -v +pio run --environment $1 -t mtjson # -v cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf echo "Copying uf2 file" cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2 -# Generate the manifest file -echo "Generating Meshtastic manifest" -TIMEFORMAT="Generated manifest in %E seconds" -time pio run --environment $1 -t mtjson --silent --disable-auto-clean -cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json +echo "Copying manifest" +cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true diff --git a/bin/build-stm32wl.sh b/bin/build-stm32wl.sh index b85da04a6..64eb36586 100755 --- a/bin/build-stm32wl.sh +++ b/bin/build-stm32wl.sh @@ -22,15 +22,12 @@ export APP_VERSION=$VERSION basename=firmware-$1-$VERSION -pio run --environment $1 # -v +pio run --environment $1 -t mtjson # -v cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf echo "Copying STM32 bin file" cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin -# Generate the manifest file -echo "Generating Meshtastic manifest" -TIMEFORMAT="Generated manifest in %E seconds" -time pio run --environment $1 -t mtjson --silent --disable-auto-clean -cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json +echo "Copying manifest" +cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true diff --git a/bin/config-dist.yaml b/bin/config-dist.yaml index b4cc81792..3c996051e 100644 --- a/bin/config-dist.yaml +++ b/bin/config-dist.yaml @@ -105,6 +105,8 @@ Lora: GPS: # SerialPath: /dev/ttyS0 +# ExtraPins: +# - 22 ### Specify I2C device, or leave blank for none @@ -184,6 +186,8 @@ Input: Logging: LogLevel: info # debug, info, warn, error # TraceFile: /var/log/meshtasticd.json +# JSONFile: /packets.json # File location for JSON output of decoded packets +# JSONFilter: position # filter for packets to save to JSON file # AsciiLogs: true # default if not specified is !isatty() on stdout Webserver: diff --git a/bin/config.d/lora-hat-rak-6421-pi-hat.yaml b/bin/config.d/lora-hat-rak-6421-pi-hat.yaml new file mode 100644 index 000000000..066e36a10 --- /dev/null +++ b/bin/config.d/lora-hat-rak-6421-pi-hat.yaml @@ -0,0 +1,11 @@ +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 diff --git a/bin/config.d/lora-usb-meshstick-1262.yaml b/bin/config.d/lora-usb-meshstick-1262.yaml new file mode 100644 index 000000000..a539d76a1 --- /dev/null +++ b/bin/config.d/lora-usb-meshstick-1262.yaml @@ -0,0 +1,14 @@ +Lora: + Module: sx1262 + CS: 0 + IRQ: 6 + Reset: 2 + Busy: 4 + RXen: 1 + DIO2_AS_RF_SWITCH: true + spidev: ch341 + DIO3_TCXO_VOLTAGE: true +# USB_Serialnum: 12345678 + USB_PID: 0x5512 + USB_VID: 0x1A86 + SX126X_MAX_POWER: 22 \ No newline at end of file diff --git a/bin/config.d/lora-usb-umesh-1262-30dbm.yaml b/bin/config.d/lora-usb-umesh-1262-30dbm.yaml new file mode 100644 index 000000000..7726eccd1 --- /dev/null +++ b/bin/config.d/lora-usb-umesh-1262-30dbm.yaml @@ -0,0 +1,23 @@ +Lora: + Module: sx1262 + CS: 0 + IRQ: 6 + Reset: 1 + Busy: 4 + RXen: 2 + DIO2_AS_RF_SWITCH: true + spidev: ch341 + USB_PID: 0x5512 + USB_VID: 0x1A86 + DIO3_TCXO_VOLTAGE: true +# USB_Serialnum: 12345678 + SX126X_MAX_POWER: 22 +# Reduce output power to improve EMI + NUM_PA_POINTS: 22 + TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7 +# Note: This module integrates an additional PA to achieve higher output power. +# The 'power' parameter here does not represent the actual RF output. +# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm). +# Each array element corresponds to the additional gain when that input level is set, +# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index]. +# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information. diff --git a/bin/config.d/lora-usb-umesh-1268-30dbm.yaml b/bin/config.d/lora-usb-umesh-1268-30dbm.yaml new file mode 100644 index 000000000..c054a92f9 --- /dev/null +++ b/bin/config.d/lora-usb-umesh-1268-30dbm.yaml @@ -0,0 +1,23 @@ +Lora: + Module: sx1268 + CS: 0 + IRQ: 6 + Reset: 1 + Busy: 4 + RXen: 2 + DIO2_AS_RF_SWITCH: true + spidev: ch341 + USB_PID: 0x5512 + USB_VID: 0x1A86 + DIO3_TCXO_VOLTAGE: true +# USB_Serialnum: 12345678 + SX126X_MAX_POWER: 22 +# Reduce output power to improve EMI + NUM_PA_POINTS: 22 + TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7 +# Note: This module integrates an additional PA to achieve higher output power. +# The 'power' parameter here does not represent the actual RF output. +# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm). +# Each array element corresponds to the additional gain when that input level is set, +# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index]. +# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information. diff --git a/bin/device-install.sh b/bin/device-install.sh index 1778a952d..49427524e 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -32,6 +32,19 @@ if ! command -v jq >/dev/null 2>&1; then exit 1 fi +# esptool v5 supports commands with dashes and deprecates commands with +# underscores. Prior versions only support commands with underscores +if ${ESPTOOL_CMD} | grep --quiet write-flash +then + ESPTOOL_WRITE_FLASH=write-flash + ESPTOOL_ERASE_FLASH=erase-flash + ESPTOOL_READ_FLASH_STATUS=read-flash-status +else + ESPTOOL_WRITE_FLASH=write_flash + ESPTOOL_ERASE_FLASH=erase_flash + ESPTOOL_READ_FLASH_STATUS=read_flash_status +fi + set -e # Usage info @@ -83,8 +96,8 @@ while [ $# -gt 0 ]; do done if [[ $BPS_RESET == true ]]; then - $ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status - exit 0 + $ESPTOOL_CMD --baud $RESET_BAUD --after no_reset ${ESPTOOL_READ_FLASH_STATUS} + exit 0 fi [ -z "$FILENAME" ] && [ -n "$1" ] && { @@ -144,12 +157,12 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then fi echo "Trying to flash ${FILENAME}, but first erasing and writing system information" - $ESPTOOL_CMD erase-flash - $ESPTOOL_CMD write-flash $FIRMWARE_OFFSET "${FILENAME}" + $ESPTOOL_CMD ${ESPTOOL_ERASE_FLASH} + $ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $FIRMWARE_OFFSET "${FILENAME}" echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}" - $ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}" + $ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OTA_OFFSET "${OTAFILE}" echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}" - $ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}" + $ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OFFSET "${SPIFFSFILE}" else show_help diff --git a/bin/device-update.sh b/bin/device-update.sh index 1c3d6be70..10eb5eedd 100755 --- a/bin/device-update.sh +++ b/bin/device-update.sh @@ -20,6 +20,17 @@ else exit 1 fi +# esptool v5 supports commands with dashes and deprecates commands with +# underscores. Prior versions only support commands with underscores +if ${ESPTOOL_CMD} | grep --quiet write-flash +then + ESPTOOL_WRITE_FLASH=write-flash + ESPTOOL_READ_FLASH_STATUS=read-flash-status +else + ESPTOOL_WRITE_FLASH=write_flash + ESPTOOL_READ_FLASH_STATUS=read_flash_status +fi + # Usage info show_help() { cat << EOF @@ -69,7 +80,7 @@ done shift "$((OPTIND-1))" if [ "$CHANGE_MODE" = true ]; then - $ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status + $ESPTOOL_CMD --baud $RESET_BAUD --after no_reset ${ESPTOOL_READ_FLASH_STATUS} exit 0 fi @@ -80,7 +91,7 @@ fi if [[ -f "$FILENAME" && "$FILENAME" != *.factory.bin ]]; then echo "Trying to flash update ${FILENAME}" - $ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}" + $ESPTOOL_CMD --baud $FLASH_BAUD ${ESPTOOL_WRITE_FLASH} $UPDATE_OFFSET "${FILENAME}" else show_help echo "Invalid file: ${FILENAME}" diff --git a/bin/generate_release_notes.py b/bin/generate_release_notes.py new file mode 100755 index 000000000..d0f1147da --- /dev/null +++ b/bin/generate_release_notes.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python3 +""" +Generate release notes from merged PRs on develop and master branches. +Categorizes PRs into Enhancements and Bug Fixes/Maintenance sections. +""" + +import subprocess +import re +import json +import sys +from datetime import datetime + + +def get_last_release_tag(): + """Get the most recent release tag.""" + result = subprocess.run( + ["git", "describe", "--tags", "--abbrev=0"], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + + +def get_tag_date(tag): + """Get the commit date (ISO 8601) of the tag.""" + result = subprocess.run( + ["git", "show", "-s", "--format=%cI", tag], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + + +def get_merged_prs_since_tag(tag, branch): + """Get all merged PRs since the given tag on the specified branch.""" + # Get commits since tag on the branch - look for PR numbers in parentheses + result = subprocess.run( + [ + "git", + "log", + f"{tag}..origin/{branch}", + "--oneline", + ], + capture_output=True, + text=True, + ) + + prs = [] + seen_pr_numbers = set() + + for line in result.stdout.strip().split("\n"): + if not line: + continue + + # Extract PR number from commit message - format: "Title (#1234)" + pr_match = re.search(r"\(#(\d+)\)", line) + if pr_match: + pr_number = pr_match.group(1) + if pr_number not in seen_pr_numbers: + seen_pr_numbers.add(pr_number) + prs.append(pr_number) + + return prs + + +def get_pr_details(pr_number): + """Get PR details from GitHub API via gh CLI.""" + try: + result = subprocess.run( + [ + "gh", + "pr", + "view", + pr_number, + "--json", + "title,author,labels,url", + ], + capture_output=True, + text=True, + check=True, + ) + return json.loads(result.stdout) + except subprocess.CalledProcessError: + return None + + +def should_exclude_pr(pr_details): + """Check if PR should be excluded from release notes.""" + if not pr_details: + return True + + title = pr_details.get("title", "").lower() + + # Exclude trunk update PRs + if "upgrade trunk" in title or "update trunk" in title or "trunk update" in title: + return True + + # Exclude protobuf update PRs + if "update protobufs" in title or "update protobuf" in title: + return True + + # Exclude automated version bump PRs + if "bump release version" in title or "bump version" in title: + return True + + return False + + +def is_dependency_update(pr_details): + """Check if PR is a dependency/chore update.""" + if not pr_details: + return False + + title = pr_details.get("title", "").lower() + author = pr_details.get("author", {}).get("login", "").lower() + labels = [label.get("name", "").lower() for label in pr_details.get("labels", [])] + + # Check for renovate or dependabot authors + if "renovate" in author or "dependabot" in author: + return True + + # Check for chore(deps) pattern + if re.match(r"^chore\(deps\):", title): + return True + + # Check for digest update patterns + if re.match(r".*digest to [a-f0-9]+", title, re.IGNORECASE): + return True + + # Check for dependency-related labels + dependency_labels = ["dependencies", "deps", "renovate"] + if any(dep in label for label in labels for dep in dependency_labels): + return True + + return False + + +def is_enhancement(pr_details): + """Determine if PR is an enhancement based on labels and title.""" + labels = [label.get("name", "").lower() for label in pr_details.get("labels", [])] + + # Check labels first + enhancement_labels = ["enhancement", "feature", "feat", "new feature"] + for label in labels: + if any(enh in label for enh in enhancement_labels): + return True + + # Check title prefixes + title = pr_details.get("title", "") + enhancement_prefixes = ["feat:", "feature:", "add:"] + title_lower = title.lower() + for prefix in enhancement_prefixes: + if title_lower.startswith(prefix) or f" {prefix}" in title_lower: + return True + + return False + + +def clean_title(title): + """Clean up PR title for release notes.""" + # Remove common prefixes + prefixes_to_remove = [ + r"^fix:\s*", + r"^feat:\s*", + r"^feature:\s*", + r"^bug:\s*", + r"^bugfix:\s*", + r"^chore:\s*", + r"^chore\([^)]+\):\s*", + r"^refactor:\s*", + r"^docs:\s*", + r"^ci:\s*", + r"^build:\s*", + r"^perf:\s*", + r"^style:\s*", + r"^test:\s*", + ] + + cleaned = title + for prefix in prefixes_to_remove: + cleaned = re.sub(prefix, "", cleaned, flags=re.IGNORECASE) + + # Ensure first letter is capitalized + if cleaned: + cleaned = cleaned[0].upper() + cleaned[1:] + + return cleaned.strip() + + +def format_pr_line(pr_details): + """Format a PR as a markdown bullet point.""" + title = clean_title(pr_details.get("title", "Unknown")) + author = pr_details.get("author", {}).get("login", "unknown") + url = pr_details.get("url", "") + + return f"- {title} by @{author} in {url}" + + +def get_new_contributors(pr_details_list, tag, repo="meshtastic/firmware"): + """Find contributors who made their first merged PR before this release. + + GitHub usernames do not necessarily match git commit authors, so we use the + GitHub search API via `gh` to see if the user has any merged PRs before the + tag date. This mirrors how GitHub's "Generate release notes" feature works. + """ + + bot_authors = {"github-actions", "renovate", "dependabot", "app/renovate", "app/github-actions", "app/dependabot"} + + new_contributors = [] + seen_authors = set() + + try: + tag_date = get_tag_date(tag) + except subprocess.CalledProcessError: + print(f"Warning: Could not determine tag date for {tag}; skipping new contributor detection", file=sys.stderr) + return [] + + for pr in pr_details_list: + author = pr.get("author", {}).get("login", "") + if not author or author in seen_authors: + continue + + # Skip bots + if author.lower() in bot_authors or author.startswith("app/"): + continue + + seen_authors.add(author) + + try: + # Search for merged PRs by this author created before the tag date + search_query = f"is:pr author:{author} repo:{repo} closed:<=\"{tag_date}\"" + search = subprocess.run( + [ + "gh", + "search", + "issues", + "--json", + "number,mergedAt,createdAt", + "--state", + "closed", + "--limit", + "200", + search_query, + ], + capture_output=True, + text=True, + ) + + if search.returncode != 0: + # If gh fails, be conservative and skip adding to new contributors + print(f"Warning: gh search failed for author {author}: {search.stderr.strip()}", file=sys.stderr) + continue + + results = json.loads(search.stdout or "[]") + # If any merged PR exists before or on tag date, not a new contributor + had_prior_pr = any(item.get("mergedAt") for item in results) + + if not had_prior_pr: + new_contributors.append((author, pr.get("url", ""))) + + except Exception as e: + print(f"Warning: Could not check contributor history for {author}: {e}", file=sys.stderr) + continue + + return new_contributors + + +def main(): + if len(sys.argv) < 2: + print("Usage: generate_release_notes.py ", file=sys.stderr) + sys.exit(1) + + new_version = sys.argv[1] + + # Get last release tag + try: + last_tag = get_last_release_tag() + except subprocess.CalledProcessError: + print("Error: Could not find last release tag", file=sys.stderr) + sys.exit(1) + + # Collect PRs from both branches + all_pr_numbers = set() + + for branch in ["develop", "master"]: + try: + prs = get_merged_prs_since_tag(last_tag, branch) + all_pr_numbers.update(prs) + except Exception as e: + print(f"Warning: Could not get PRs from {branch}: {e}", file=sys.stderr) + + # Get details for all PRs + enhancements = [] + bug_fixes = [] + dependencies = [] + all_pr_details = [] + + for pr_number in sorted(all_pr_numbers, key=int): + details = get_pr_details(pr_number) + if details and not should_exclude_pr(details): + all_pr_details.append(details) + if is_dependency_update(details): + dependencies.append(details) + elif is_enhancement(details): + enhancements.append(details) + else: + bug_fixes.append(details) + + # Generate release notes + output = [] + + if enhancements: + output.append("## 🚀 Enhancements\n") + for pr in enhancements: + output.append(format_pr_line(pr)) + output.append("") + + if bug_fixes: + output.append("## 🐛 Bug fixes and maintenance\n") + for pr in bug_fixes: + output.append(format_pr_line(pr)) + output.append("") + + if dependencies: + output.append("## ⚙️ Dependencies\n") + for pr in dependencies: + output.append(format_pr_line(pr)) + output.append("") + + # Find new contributors (GitHub-accurate check using merged PRs before tag date) + new_contributors = get_new_contributors(all_pr_details, last_tag) + if new_contributors: + output.append("## New Contributors\n") + for author, url in new_contributors: + # Find first PR URL for this contributor + first_pr_url = url + for pr in all_pr_details: + if pr.get("author", {}).get("login") == author: + first_pr_url = pr.get("url", url) + break + output.append(f"- @{author} made their first contribution in {first_pr_url}") + output.append("") + + # Add full changelog link + output.append( + f"**Full Changelog**: https://github.com/meshtastic/firmware/compare/{last_tag}...v{new_version}" + ) + + print("\n".join(output)) + + +if __name__ == "__main__": + main() diff --git a/bin/meshtasticd-start.sh b/bin/meshtasticd-start.sh new file mode 100755 index 000000000..b58d92085 --- /dev/null +++ b/bin/meshtasticd-start.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env sh + +INSTANCE=$1 +CONF_DIR="/etc/meshtasticd/config.d" +VFS_DIR="/var/lib" + +# If no instance ID provided, start bare daemon and exit +echo "no instance ID provided, starting bare meshtasticd service" +if [ -z "${INSTANCE}" ]; then + /usr/bin/meshtasticd + exit 0 +fi + +# Make VFS dir if it does not exist +if [ ! -d "${VFS_DIR}/meshtasticd-${INSTANCE}" ]; then + echo "vfs for ${INSTANCE} does not exist, creating it." + mkdir "${VFS_DIR}/meshtasticd-${INSTANCE}" +fi + +# Abort if config for $INSTANCE does not exist +if [ ! -f "${CONF_DIR}/config-${INSTANCE}.yaml" ]; then + echo "no config for ${INSTANCE} found in ${CONF_DIR}. refusing to start" >&2 + exit 1 +fi + +# Start meshtasticd with instance parameters +printf "starting meshtasticd-%s..., ${INSTANCE}" +if /usr/bin/meshtasticd --config="${CONF_DIR}/config-${INSTANCE}.yaml" --fsdir="${VFS_DIR}/meshtasticd-${INSTANCE}"; then + echo "ok" +else + echo "failed" +fi diff --git a/bin/meshtasticd.service b/bin/meshtasticd.service index 63430bae8..8ca32a8aa 100644 --- a/bin/meshtasticd.service +++ b/bin/meshtasticd.service @@ -1,5 +1,5 @@ [Unit] -Description=Meshtastic Native Daemon +Description=Meshtastic %i Daemon After=network-online.target StartLimitInterval=200 StartLimitBurst=5 @@ -9,7 +9,7 @@ AmbientCapabilities=CAP_NET_BIND_SERVICE User=meshtasticd Group=meshtasticd Type=simple -ExecStart=/usr/bin/meshtasticd +ExecStart=/usr/bin/meshtasticd-start.sh %i Restart=always RestartSec=3 diff --git a/bin/native-install.sh b/bin/native-install.sh index 18cd9205b..dfcb7b40d 100755 --- a/bin/native-install.sh +++ b/bin/native-install.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash cp "release/meshtasticd_linux_$(uname -m)" /usr/bin/meshtasticd +cp "bin/meshtasticd-start.sh" /usr/bin/meshtasticd-start.sh mkdir -p /etc/meshtasticd if [[ -f "/etc/meshtasticd/config.yaml" ]]; then cp bin/config-dist.yaml /etc/meshtasticd/config-upgrade.yaml diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index 140ac3e2a..6ad8962d1 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,12 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.19 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.18 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.17 diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py index 151cf0a97..b75c66624 100644 --- a/bin/platformio-custom.py +++ b/bin/platformio-custom.py @@ -2,11 +2,12 @@ # trunk-ignore-all(ruff/F821) # trunk-ignore-all(flake8/F821): For SConstruct imports import sys -from os.path import join, basename, isfile +from os.path import join import subprocess import json import re from datetime import datetime +from typing import Dict from readprops import readProps @@ -14,9 +15,59 @@ Import("env") platform = env.PioPlatform() progname = env.get("PROGNAME") lfsbin = f"{progname.replace('firmware-', 'littlefs-')}.bin" +manifest_ran = False + +def infer_architecture(board_cfg): + try: + mcu = board_cfg.get("build.mcu") if board_cfg else None + except KeyError: + mcu = None + except Exception: + mcu = None + if not mcu: + return None + mcu_l = str(mcu).lower() + if "esp32s3" in mcu_l: + return "esp32-s3" + if "esp32c6" in mcu_l: + return "esp32-c6" + if "esp32c3" in mcu_l: + return "esp32-c3" + if "esp32" in mcu_l: + return "esp32" + if "rp2040" in mcu_l: + return "rp2040" + if "rp2350" in mcu_l: + return "rp2350" + if "nrf52" in mcu_l or "nrf52840" in mcu_l: + return "nrf52840" + if "stm32" in mcu_l: + return "stm32" + return None def manifest_gather(source, target, env): + global manifest_ran + if manifest_ran: + return + # Skip manifest generation if we cannot determine architecture (host/native builds) + board_arch = infer_architecture(env.BoardConfig()) + if not board_arch: + print(f"Skipping mtjson generation for unknown architecture (env={env.get('PIOENV')})") + manifest_ran = True + return + manifest_ran = True out = [] + board_platform = env.BoardConfig().get("platform") + board_mcu = env.BoardConfig().get("build.mcu").lower() + needs_ota_suffix = board_platform == "nordicnrf52" + + # Mapping of bin files to their target partition names + # Maps the filename pattern to the partition name where it should be flashed + partition_map = { + f"{progname}.bin": "app0", # primary application slot (app0 / OTA_0) + lfsbin: "spiffs", # filesystem image flashed to spiffs + } + check_paths = [ progname, f"{progname}.elf", @@ -27,29 +78,62 @@ def manifest_gather(source, target, env): f"{progname}.uf2", f"{progname}.factory.uf2", f"{progname}.zip", - lfsbin + lfsbin, + f"mt-{board_mcu}-ota.bin", + "bleota-c3.bin" ] for p in check_paths: f = env.File(env.subst(f"$BUILD_DIR/{p}")) if f.exists(): + manifest_name = p + if needs_ota_suffix and p == f"{progname}.zip": + manifest_name = f"{progname}-ota.zip" d = { - "name": p, + "name": manifest_name, "md5": f.get_content_hash(), # Returns MD5 hash "bytes": f.get_size() # Returns file size in bytes } + # Add part_name if this file represents a partition that should be flashed + if p in partition_map: + d["part_name"] = partition_map[p] out.append(d) print(d) manifest_write(out, env) def manifest_write(files, env): + # Defensive: also skip manifest writing if we cannot determine architecture + def get_project_option(name): + try: + return env.GetProjectOption(name) + except Exception: + return None + + def get_project_option_any(names): + for name in names: + val = get_project_option(name) + if val is not None: + return val + return None + + def as_bool(val): + return str(val).strip().lower() in ("1", "true", "yes", "on") + + def as_int(val): + try: + return int(str(val), 10) + except (TypeError, ValueError): + return None + + def as_list(val): + return [item.strip() for item in str(val).split(",") if item.strip()] + manifest = { "version": verObj["long"], "build_epoch": build_epoch, - "board": env.get("PIOENV"), + "platformioTarget": env.get("PIOENV"), "mcu": env.get("BOARD_MCU"), "repo": repo_owner, "files": files, - "part": None, "has_mui": False, "has_inkhud": False, } @@ -64,6 +148,51 @@ def manifest_write(files, env): if "MESHTASTIC_INCLUDE_INKHUD" in env.get("CPPDEFINES", []): manifest["has_inkhud"] = True + pioenv = env.get("PIOENV") + device_meta = {} + device_meta_fields = [ + ("hwModel", ["custom_meshtastic_hw_model"], as_int), + ("hwModelSlug", ["custom_meshtastic_hw_model_slug"], str), + ("architecture", ["custom_meshtastic_architecture"], str), + ("activelySupported", ["custom_meshtastic_actively_supported"], as_bool), + ("displayName", ["custom_meshtastic_display_name"], str), + ("supportLevel", ["custom_meshtastic_support_level"], as_int), + ("images", ["custom_meshtastic_images"], as_list), + ("tags", ["custom_meshtastic_tags"], as_list), + ("requiresDfu", ["custom_meshtastic_requires_dfu"], as_bool), + ("partitionScheme", ["custom_meshtastic_partition_scheme"], str), + ("url", ["custom_meshtastic_url"], str), + ("key", ["custom_meshtastic_key"], str), + ("variant", ["custom_meshtastic_variant"], str), + ] + + + for manifest_key, option_keys, caster in device_meta_fields: + raw_val = get_project_option_any(option_keys) + if raw_val is None: + continue + parsed = caster(raw_val) if callable(caster) else raw_val + if parsed is not None and parsed != "": + device_meta[manifest_key] = parsed + + # Determine architecture once; if we can't infer it, skip manifest generation + board_arch = device_meta.get("architecture") or infer_architecture(env.BoardConfig()) + if not board_arch: + print(f"Skipping mtjson write for unknown architecture (env={env.get('PIOENV')})") + return + + device_meta["architecture"] = board_arch + + # Always set requiresDfu: true for nrf52840 targets + if board_arch == "nrf52840": + device_meta["requiresDfu"] = True + + device_meta.setdefault("displayName", pioenv) + device_meta.setdefault("activelySupported", False) + + if device_meta: + manifest.update(device_meta) + # Write the manifest to the build directory with open(env.subst("$BUILD_DIR/${PROGNAME}.mt.json"), "w") as f: json.dump(manifest, f, indent=2) @@ -159,20 +288,42 @@ def load_boot_logo(source, target, env): # Load the boot logo on TFT builds if ("HAS_TFT", 1) in env.get("CPPDEFINES", []): - env.AddPreAction('$BUILD_DIR/littlefs.bin', load_boot_logo) + env.AddPreAction(f"$BUILD_DIR/{lfsbin}", load_boot_logo) -# Rename (mv) littlefs.bin to include the PROGNAME -# This ensures the littlefs.bin is named consistently with the firmware -env.AddPostAction('$BUILD_DIR/littlefs.bin', env.VerboseAction( - f'mv $BUILD_DIR/littlefs.bin $BUILD_DIR/{lfsbin}', - f'Renaming littlefs.bin to {lfsbin}' -)) +board_arch = infer_architecture(env.BoardConfig()) +should_skip_manifest = board_arch is None -env.AddCustomTarget( - name="mtjson", - dependencies=None, - actions=[manifest_gather], - title="Meshtastic Manifest", - description="Generating Meshtastic manifest JSON + Checksums", - always_build=True, -) +# For host/native envs, avoid depending on 'buildprog' (some targets don't define it) +mtjson_deps = [] if should_skip_manifest else ["buildprog"] +if not should_skip_manifest and platform.name == "espressif32": + # Build littlefs image as part of mtjson target + # Equivalent to `pio run -t buildfs` + target_lfs = env.DataToBin( + join("$BUILD_DIR", "${ESP32_FS_IMAGE_NAME}"), "$PROJECT_DATA_DIR" + ) + mtjson_deps.append(target_lfs) + +if should_skip_manifest: + def skip_manifest(source, target, env): + print(f"mtjson: skipped for native environment: {env.get('PIOENV')}") + + env.AddCustomTarget( + name="mtjson", + dependencies=mtjson_deps, + actions=[skip_manifest], + title="Meshtastic Manifest (skipped)", + description="mtjson generation is skipped for native environments", + always_build=True, + ) +else: + env.AddCustomTarget( + name="mtjson", + dependencies=mtjson_deps, + actions=[manifest_gather], + title="Meshtastic Manifest", + description="Generating Meshtastic manifest JSON + Checksums", + always_build=True, + ) + + # Run manifest generation as part of the default build pipeline for non-native builds. + env.Default("mtjson") diff --git a/bin/platformio-pre.py b/bin/platformio-pre.py index 4e51a6544..16278b813 100644 --- a/bin/platformio-pre.py +++ b/bin/platformio-pre.py @@ -11,6 +11,9 @@ else: prefsLoc = env["PROJECT_DIR"] + "/version.properties" verObj = readProps(prefsLoc) env.Replace(PROGNAME=f"firmware-{env.get('PIOENV')}-{verObj['long']}") + env.Replace(ESP32_FS_IMAGE_NAME=f"littlefs-{env.get('PIOENV')}-{verObj['long']}") # Print the new program name for verification print(f"PROGNAME: {env.get('PROGNAME')}") +if platform.name == "espressif32": + print(f"ESP32_FS_IMAGE_NAME: {env.get('ESP32_FS_IMAGE_NAME')}") diff --git a/bin/readprops.py b/bin/readprops.py index 731a3d0d3..4b92d63dd 100644 --- a/bin/readprops.py +++ b/bin/readprops.py @@ -18,8 +18,9 @@ def readProps(prefsLoc): # Try to find current build SHA if if the workspace is clean. This could fail if git is not installed try: + # Pin abbreviation length to keep local builds and CI matching (avoid auto-shortening) sha = ( - subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]) + subprocess.check_output(["git", "rev-parse", "--short=7", "HEAD"]) .decode("utf-8") .strip() ) diff --git a/bin/shame.py b/bin/shame.py new file mode 100644 index 000000000..f2253bfdc --- /dev/null +++ b/bin/shame.py @@ -0,0 +1,95 @@ +import sys +import os +import json +from github import Github + +def parseFile(path): + with open(path, "r") as f: + data = json.loads(f) + for file in data["files"]: + if file["name"].endswith(".bin"): + return file["name"], file["bytes"] + +if len(sys.argv) != 4: + print(f"expected usage: {sys.argv[0]} ") + sys.exit(1) + +pr_number = int(sys.argv[1]) + +token = os.getenv("GITHUB_TOKEN") +if not token: + raise EnvironmentError("GITHUB_TOKEN not found in environment.") + +repo_name = os.getenv("GITHUB_REPOSITORY") # "owner/repo" +if not repo_name: + raise EnvironmentError("GITHUB_REPOSITORY not found in environment.") + +oldFiles = sys.argv[2] +old = set(os.path.join(oldFiles, f) for f in os.listdir(oldFiles) if os.path.isfile(f)) +newFiles = sys.argv[3] +new = set(os.path.join(newFiles, f) for f in os.listdir(newFiles) if os.path.isfile(f)) + +startMarkdown = "# Target Size Changes\n\n" +markdown = "" + +newlyIntroduced = new - old +if len(newlyIntroduced) > 0: + markdown += "## Newly Introduced Targets\n\n" + # create a table + markdown += "| File | Size |\n" + markdown += "| ---- | ---- |\n" + for f in newlyIntroduced: + name, size = parseFile(f) + markdown += f"| `{name}` | {size}b |\n" + +# do not log removed targets +# PRs only run a small subset of builds, so removed targets are not meaningful +# since they are very likely to just be not ran in PR CI + +both = old & new +degradations = [] +improvements = [] +for f in both: + oldName, oldSize = parseFile(f) + _, newSize = parseFile(f) + if oldSize != newSize: + if newSize < oldSize: + improvements.append((oldName, oldSize, newSize)) + else: + degradations.append((oldName, oldSize, newSize)) + +if len(degradations) > 0: + markdown += "\n## Degradation\n\n" + # create a table + markdown += "| File | Difference | Old Size | New Size |\n" + markdown += "| ---- | ---------- | -------- | -------- |\n" + for oldName, oldSize, newSize in degradations: + markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n" + +if len(improvements) > 0: + markdown += "\n## Improvement\n\n" + # create a table + markdown += "| File | Difference | Old Size | New Size |\n" + markdown += "| ---- | ---------- | -------- | -------- |\n" + for oldName, oldSize, newSize in improvements: + markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n" + +if len(markdown) == 0: + markdown = "No changes in target sizes detected." + +g = Github(token) +repo = g.get_repo(repo_name) +pr = repo.get_pull(pr_number) + +existing_comment = None +for comment in pr.get_issue_comments(): + if comment.body.startswith(startMarkdown): + existing_comment = comment + break + +final_markdown = startMarkdown + markdown + +if existing_comment: + existing_comment.edit(body=final_markdown) +else: + pr.create_issue_comment(body=final_markdown) diff --git a/boards/CDEBYTE_EoRa-Hub.json b/boards/CDEBYTE_EoRa-Hub.json new file mode 100644 index 000000000..66e2cae95 --- /dev/null +++ b/boards/CDEBYTE_EoRa-Hub.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default.csv", + "memory_type": "qio_qspi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "CDEBYTE_EoRa-Hub", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.cdebyte.com/products/EoRa-HUB-900TB", + "vendor": "CDEBYTE" +} diff --git a/boards/ThinkNode-M4.json b/boards/ThinkNode-M4.json new file mode 100644 index 000000000..178bfaee9 --- /dev/null +++ b/boards/ThinkNode-M4.json @@ -0,0 +1,53 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_ELECROW_M4 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"] + ], + "usb_product": "elecrow_thinknode_m4", + "mcu": "nrf52840", + "variant": "ELECROW-ThinkNode-M4", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "ELECROW ThinkNode m4", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.elecrow.com/thinknode-m4-power-bank-lora-device-with-meshtastic-lora-tracker-function-powered-by-nrf52840.html", + "vendor": "ELECROW" +} diff --git a/boards/minimesh_lite.json b/boards/minimesh_lite.json new file mode 100644 index 000000000..0b8f0b909 --- /dev/null +++ b/boards/minimesh_lite.json @@ -0,0 +1,50 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DMINIMESH_LITE -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"] + ], + "usb_product": "Minimesh Lite", + "mcu": "nrf52840", + "variant": "dls_Minimesh_Lite", + "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" + }, + "frameworks": ["arduino"], + "name": "Minimesh Lite", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["nrfutil", "jlink", "nrfjprog", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://deeplabstudio.com", + "vendor": "Deeplab Studio" +} diff --git a/boards/t-beam-1w.json b/boards/t-beam-1w.json new file mode 100644 index 000000000..40f16195d --- /dev/null +++ b/boards/t-beam-1w.json @@ -0,0 +1,39 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DLILYGO_TBEAM_1W", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "t-beam-1w" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino"], + "name": "LilyGo TBeam-1W", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "http://www.lilygo.cn/", + "vendor": "LilyGo" +} diff --git a/boards/t-watch-s3.json b/boards/t-watch-s3.json index bae4f47b0..f3c0bea8e 100644 --- a/boards/t-watch-s3.json +++ b/boards/t-watch-s3.json @@ -9,7 +9,7 @@ "-DBOARD_HAS_PSRAM", "-DT_WATCH_S3", "-DARDUINO_USB_CDC_ON_BOOT=1", - "-DARDUINO_USB_MODE=0", + "-DARDUINO_USB_MODE=1", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1" ], diff --git a/debian/changelog b/debian/changelog index b9212c1be..38489b074 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +meshtasticd (2.7.19.0) unstable; urgency=medium + + * Version 2.7.19 + + -- GitHub Actions Thu, 22 Jan 2026 22:17:40 +0000 + +meshtasticd (2.7.18.0) unstable; urgency=medium + + * Version 2.7.18 + + -- GitHub Actions Fri, 02 Jan 2026 12:45:36 +0000 + meshtasticd (2.7.17.0) unstable; urgency=medium * Version 2.7.17 diff --git a/debian/control b/debian/control index 679a444c9..46c932a80 100644 --- a/debian/control +++ b/debian/control @@ -25,7 +25,8 @@ Build-Depends: debhelper-compat (= 13), liborcania-dev, libx11-dev, libinput-dev, - libxkbcommon-x11-dev + libxkbcommon-x11-dev, + libsqlite3-dev Standards-Version: 4.6.2 Homepage: https://github.com/meshtastic/firmware Rules-Requires-Root: no diff --git a/debian/meshtasticd.install b/debian/meshtasticd.install index 3c68b42b1..62c0150db 100644 --- a/debian/meshtasticd.install +++ b/debian/meshtasticd.install @@ -4,5 +4,6 @@ bin/config.yaml etc/meshtasticd bin/config.d/* etc/meshtasticd/available.d bin/meshtasticd.service lib/systemd/system +bin/meshtasticd-start.sh usr/bin web/* usr/share/meshtasticd/web diff --git a/extra_scripts/esp32_extra.py b/extra_scripts/esp32_extra.py index 8841ad1dc..f7698561a 100755 --- a/extra_scripts/esp32_extra.py +++ b/extra_scripts/esp32_extra.py @@ -10,6 +10,12 @@ Import("env") platform = env.PioPlatform() sys.path.append(join(platform.get_package_dir("tool-esptoolpy"))) +# IntelHex workaround, remove after fixed upstream +# https://github.com/platformio/platform-espressif32/issues/1632 +try: + import intelhex +except ImportError: + env.Execute("$PYTHONEXE -m pip install intelhex") import esptool diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..e7a4c7ff7 --- /dev/null +++ b/flake.lock @@ -0,0 +1,44 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1766314097, + "narHash": "sha256-laJftWbghBehazn/zxVJ8NdENVgjccsWAdAqKXhErrM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "306ea70f9eb0fb4e040f8540e2deab32ed7e2055", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..1af493c6d --- /dev/null +++ b/flake.nix @@ -0,0 +1,66 @@ +{ + description = "Nix flake to compile Meshtastic firmware"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + # Shim to make flake.nix work with stable Nix. + flake-compat = { + url = "github:NixOS/flake-compat"; + flake = false; + }; + }; + + outputs = + inputs: + let + lib = inputs.nixpkgs.lib; + + forAllSystems = + fn: + lib.genAttrs lib.systems.flakeExposed ( + system: + fn { + pkgs = import inputs.nixpkgs { + inherit system; + }; + inherit system; + } + ); + in + { + devShells = forAllSystems ( + { pkgs, ... }: + let + python3 = pkgs.python312.withPackages ( + ps: with ps; [ + google + ] + ); + in + { + default = pkgs.mkShell { + buildInputs = with pkgs; [ + python3 + platformio + ]; + + shellHook = '' + # Set up PlatformIO to use a local core directory. + export PLATFORMIO_CORE_DIR=$PWD/.platformio + # Tell pip to put packages into $PIP_PREFIX instead of the usual + # location. This is especially necessary under NixOS to avoid having + # pip trying to write to the read-only Nix store. For more info, + # see https://wiki.nixos.org/wiki/Python + export PIP_PREFIX=$PWD/.python3 + export PYTHONPATH="$PIP_PREFIX/${python3.sitePackages}" + export PATH="$PIP_PREFIX/bin:$PATH" + # Avoids reproducibility issues with some Python packages + # See https://nixos.org/manual/nixpkgs/stable/#python-setup.py-bdist_wheel-cannot-create-.whl + unset SOURCE_DATE_EPOCH + ''; + }; + } + ); + }; +} diff --git a/meshtasticd.spec.rpkg b/meshtasticd.spec.rpkg index 3456001f0..fc14ede7f 100644 --- a/meshtasticd.spec.rpkg +++ b/meshtasticd.spec.rpkg @@ -39,6 +39,7 @@ BuildRequires: pkgconfig(bluez) BuildRequires: pkgconfig(libusb-1.0) BuildRequires: libi2c-devel BuildRequires: pkgconfig(libuv) +BuildRequires: pkgconfig(sqlite3) # Web components: BuildRequires: pkgconfig(openssl) BuildRequires: pkgconfig(liborcania) @@ -95,6 +96,9 @@ cp -r bin/config.d/* %{buildroot}%{_sysconfdir}/meshtasticd/available.d # Install systemd service install -D -m 0644 bin/meshtasticd.service %{buildroot}%{_unitdir}/meshtasticd.service +# Install meshtasticd start wrapper +install -D -m 0755 bin/meshtasticd-start.sh %{buildroot}%{_bindir}/meshtasticd-start.sh + # Install the web files under /usr/share/meshtasticd/web mkdir -p %{buildroot}%{_datadir}/meshtasticd/web cp -r web/* %{buildroot}%{_datadir}/meshtasticd/web diff --git a/monitor/filter_c3_exception_decoder.py b/monitor/filter_c3_exception_decoder.py index 5e74dc2b9..fbc372bcf 100644 --- a/monitor/filter_c3_exception_decoder.py +++ b/monitor/filter_c3_exception_decoder.py @@ -43,13 +43,11 @@ class Esp32C3ExceptionDecoder(DeviceMonitorFilterBase): self.enabled = self.setup_paths() if self.config.get("env:" + self.environment, "build_type") != "debug": - print( - """ + print(""" Please build project in debug configuration to get more details about an exception. See https://docs.platformio.org/page/projectconf/build_configurations.html -""" - ) +""") return self diff --git a/platformio.ini b/platformio.ini index 25997e11d..77e9cf214 100644 --- a/platformio.ini +++ b/platformio.ini @@ -54,7 +54,9 @@ 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 + -DMESHTASTIC_EXCLUDE_POWERMON=1 -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage + -DLED_BUILTIN=-1 #-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now #-D OLED_PL=1 #-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs @@ -64,7 +66,7 @@ monitor_speed = 115200 monitor_filters = direct lib_deps = # renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master - https://github.com/meshtastic/esp8266-oled-ssd1306/archive/2887bf4a19f64d92c984dcc8fd5ca7429e425e4a.zip + https://github.com/meshtastic/esp8266-oled-ssd1306/archive/b34c6817c25d6faabb3a8a162b5d14fb75395433.zip # renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip # renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master @@ -103,27 +105,22 @@ lib_deps = thingsboard/TBPubSubClient@2.12.1 # renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient arduino-libraries/NTPClient@3.2.1 + +; Extra TCP/IP networking libs for supported devices +[networking_extra] +lib_deps = # renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog arcao/Syslog@2.0.0 -; Minimal networking libs for nrf52 (excludes Syslog to save flash) -[nrf52_networking_base] -lib_deps = - # renovate: datasource=custom.pio depName=TBPubSubClient packageName=thingsboard/library/TBPubSubClient - thingsboard/TBPubSubClient@2.12.1 - # renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient - arduino-libraries/NTPClient@3.2.1 - [radiolib_base] lib_deps = # renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib - # jgromes/RadioLib@7.4.0 - https://github.com/jgromes/RadioLib/archive/536c7267362e2c1345be7054ba45e503252975ff.zip + jgromes/RadioLib@7.5.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/4fb5f24787caa841b58dbf623a52c4c5861d6722.zip + https://github.com/meshtastic/device-ui/archive/63967a4a557d33d56fc5746f9128200dde2d88c5.zip ; Common libs for environmental measurements in telemetry module [environmental_base] @@ -133,7 +130,7 @@ lib_deps = # renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor adafruit/Adafruit Unified Sensor@1.1.15 # renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library - adafruit/Adafruit BMP280 Library@2.6.8 + adafruit/Adafruit BMP280 Library@3.0.0 # renovate: datasource=custom.pio depName=Adafruit BMP085 packageName=adafruit/library/Adafruit BMP085 Library adafruit/Adafruit BMP085 Library@1.2.4 # renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library @@ -146,8 +143,6 @@ lib_deps = adafruit/Adafruit INA260 Library@1.5.3 # renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219 adafruit/Adafruit INA219@1.2.3 - # renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor - adafruit/Adafruit PM25 AQI Sensor@2.0.0 # renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050 adafruit/Adafruit MPU6050@2.2.6 # renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH @@ -162,8 +157,8 @@ lib_deps = emotibit/EmotiBit MLX90632@1.0.8 # renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library adafruit/Adafruit MLX90614 Library@2.1.5 - # renovate: datasource=github-tags depName=INA3221 packageName=sgtwilko/INA3221 - https://github.com/sgtwilko/INA3221#bb03d7e9bfcc74fc798838a54f4f99738f29fc6a + # renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow + https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip # renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass mprograms/QMC5883LCompass@1.2.3 # renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU @@ -171,7 +166,7 @@ lib_deps = # renovate: datasource=git-refs depName=DFRobot_RainfallSensor packageName=https://github.com/DFRobot/DFRobot_RainfallSensor gitBranch=master https://github.com/DFRobot/DFRobot_RainfallSensor/archive/38fea5e02b40a5430be6dab39a99a6f6347d667e.zip # renovate: datasource=custom.pio depName=INA226 packageName=robtillaart/library/INA226 - robtillaart/INA226@0.6.5 + robtillaart/INA226@0.6.6 # renovate: datasource=custom.pio depName=SparkFun MAX3010x packageName=sparkfun/library/SparkFun MAX3010x Pulse and Proximity Sensor Library 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 @@ -218,3 +213,30 @@ lib_deps = sensirion/Sensirion Core@0.7.2 # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x sensirion/Sensirion I2C SCD4x@1.1.0 +; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets) +[environmental_extra_no_bsec] +lib_deps = + # renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library + adafruit/Adafruit BMP3XX Library@2.1.6 + # renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X + adafruit/Adafruit MAX1704X@1.0.3 + # renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library + adafruit/Adafruit SHTC3 Library@1.0.2 + # renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X + adafruit/Adafruit LPS2X@2.0.6 + # renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library + adafruit/Adafruit SHT31 Library@2.2.2 + # renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library + adafruit/Adafruit VEML7700 Library@2.1.6 + # renovate: datasource=custom.pio depName=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library + adafruit/Adafruit SHT4x Library@1.0.5 + # renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library + sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6 + # renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001 + closedcube/ClosedCube OPT3001@1.1.2 + # 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 + # renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core + sensirion/Sensirion Core@0.7.2 + # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x + sensirion/Sensirion I2C SCD4x@1.1.0 \ No newline at end of file diff --git a/protobufs b/protobufs index 4095e5989..bc63a57f9 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 4095e598902b4cd893dbcb62842514704d0f64e0 +Subproject commit bc63a57f9e5dba8a7c90ee0bd4a9840862d61f6d diff --git a/shell.nix b/shell.nix new file mode 100644 index 000000000..692cd4df8 --- /dev/null +++ b/shell.nix @@ -0,0 +1,12 @@ +(import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + nodeName = lock.nodes.root.inputs.flake-compat; + in + fetchTarball { + url = + lock.nodes.${nodeName}.locked.url + or "https://github.com/NixOS/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz"; + sha256 = lock.nodes.${nodeName}.locked.narHash; + } +) { src = ./.; }).shellNix diff --git a/src/BluetoothStatus.h b/src/BluetoothStatus.h index 680aec929..4ea4a95ac 100644 --- a/src/BluetoothStatus.h +++ b/src/BluetoothStatus.h @@ -89,22 +89,14 @@ class BluetoothStatus : public Status case ConnectionState::CONNECTED: LOG_DEBUG("BluetoothStatus CONNECTED"); #ifdef BLE_LED -#ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, LOW); -#else - digitalWrite(BLE_LED, HIGH); -#endif + digitalWrite(BLE_LED, LED_STATE_ON); #endif break; case ConnectionState::DISCONNECTED: LOG_DEBUG("BluetoothStatus DISCONNECTED"); #ifdef BLE_LED -#ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); -#else - digitalWrite(BLE_LED, LOW); -#endif + digitalWrite(BLE_LED, LED_STATE_OFF); #endif break; } diff --git a/src/DebugConfiguration.cpp b/src/DebugConfiguration.cpp index d65c4f1e8..08c7abc04 100644 --- a/src/DebugConfiguration.cpp +++ b/src/DebugConfiguration.cpp @@ -41,7 +41,8 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...) } #if HAS_NETWORKING - +namespace meshtastic +{ Syslog::Syslog(UDP &client) { this->_client = &client; @@ -195,4 +196,6 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess return true; } +}; // namespace meshtastic + #endif diff --git a/src/DebugConfiguration.h b/src/DebugConfiguration.h index 98bbe0f72..eac6260fc 100644 --- a/src/DebugConfiguration.h +++ b/src/DebugConfiguration.h @@ -162,6 +162,8 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...); #if HAS_NETWORKING +namespace meshtastic +{ class Syslog { private: @@ -195,4 +197,6 @@ class Syslog bool vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) __attribute__((format(printf, 3, 0))); }; -#endif // HAS_NETWORKING \ No newline at end of file +}; // namespace meshtastic + +#endif // HAS_NETWORKING diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index 246cf0022..d88f9fc9f 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -31,6 +31,9 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: return useShortName ? "LongF" : "LongFast"; break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: + return useShortName ? "LongT" : "LongTurbo"; + break; case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: return useShortName ? "LongM" : "LongMod"; break; diff --git a/src/MessageStore.cpp b/src/MessageStore.cpp new file mode 100644 index 000000000..22da418f5 --- /dev/null +++ b/src/MessageStore.cpp @@ -0,0 +1,514 @@ +#include "configuration.h" +#if HAS_SCREEN +#include "FSCommon.h" +#include "MessageStore.h" +#include "NodeDB.h" +#include "SPILock.h" +#include "SafeFile.h" +#include "gps/RTC.h" +#include "graphics/draw/MessageRenderer.h" +#include // memcpy + +#ifndef MESSAGE_TEXT_POOL_SIZE +#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE) +#endif + +// Default autosave interval 2 hours, override per device later with -DMESSAGE_AUTOSAVE_INTERVAL_SEC=300 (etc) +#ifndef MESSAGE_AUTOSAVE_INTERVAL_SEC +#define MESSAGE_AUTOSAVE_INTERVAL_SEC (2 * 60 * 60) +#endif + +// Global message text pool and state +static char *g_messagePool = nullptr; +static size_t g_poolWritePos = 0; + +// Reset pool (called on boot or clear) +static inline void resetMessagePool() +{ + if (!g_messagePool) { + g_messagePool = static_cast(malloc(MESSAGE_TEXT_POOL_SIZE)); + if (!g_messagePool) { + LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE); + return; + } + } + g_poolWritePos = 0; + memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE); +} + +// Allocate text in pool and return offset +// If not enough space remains, wrap around (ring buffer style) +static inline uint16_t storeTextInPool(const char *src, size_t len) +{ + if (len >= MAX_MESSAGE_SIZE) + len = MAX_MESSAGE_SIZE - 1; + + // Wrap pool if out of space + if (g_poolWritePos + len + 1 >= MESSAGE_TEXT_POOL_SIZE) { + g_poolWritePos = 0; + } + + uint16_t offset = g_poolWritePos; + memcpy(&g_messagePool[g_poolWritePos], src, len); + g_messagePool[g_poolWritePos + len] = '\0'; + g_poolWritePos += (len + 1); + return offset; +} + +// Retrieve a const pointer to message text by offset +static inline const char *getTextFromPool(uint16_t offset) +{ + if (!g_messagePool || offset >= MESSAGE_TEXT_POOL_SIZE) + return ""; + return &g_messagePool[offset]; +} + +// Helper: assign a timestamp (RTC if available, else boot-relative) +static inline void assignTimestamp(StoredMessage &sm) +{ + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + if (nowSecs) { + sm.timestamp = nowSecs; + sm.isBootRelative = false; + } else { + sm.timestamp = millis() / 1000; + sm.isBootRelative = true; + } +} + +// Generic push with cap (used by live + persisted queues) +template static inline void pushWithLimit(std::deque &queue, const T &msg) +{ + if (queue.size() >= MAX_MESSAGES_SAVED) + queue.pop_front(); + queue.push_back(msg); +} + +template static inline void pushWithLimit(std::deque &queue, T &&msg) +{ + if (queue.size() >= MAX_MESSAGES_SAVED) + queue.pop_front(); + queue.emplace_back(std::move(msg)); +} + +MessageStore::MessageStore(const std::string &label) +{ + filename = "/Messages_" + label + ".msgs"; + resetMessagePool(); // initialize text pool on boot +} + +// Live message handling (RAM only) +void MessageStore::addLiveMessage(StoredMessage &&msg) +{ + pushWithLimit(liveMessages, std::move(msg)); +} +void MessageStore::addLiveMessage(const StoredMessage &msg) +{ + pushWithLimit(liveMessages, msg); +} + +#if ENABLE_MESSAGE_PERSISTENCE +static bool g_messageStoreHasUnsavedChanges = false; +static uint32_t g_lastAutoSaveMs = 0; // last time we actually saved + +static inline uint32_t autosaveIntervalMs() +{ + uint32_t sec = (uint32_t)MESSAGE_AUTOSAVE_INTERVAL_SEC; + if (sec < 60) + sec = 60; + return sec * 1000UL; +} + +static inline bool reachedMs(uint32_t now, uint32_t target) +{ + return (int32_t)(now - target) >= 0; +} + +// Mark new messages in RAM that need to be saved later +static inline void markMessageStoreUnsaved() +{ + g_messageStoreHasUnsavedChanges = true; + + if (g_lastAutoSaveMs == 0) { + g_lastAutoSaveMs = millis(); + } +} + +// Called periodically from the main loop in main.cpp +static inline void autosaveTick(MessageStore *store) +{ + if (!store) + return; + + uint32_t now = millis(); + + if (g_lastAutoSaveMs == 0) { + g_lastAutoSaveMs = now; + return; + } + + if (!reachedMs(now, g_lastAutoSaveMs + autosaveIntervalMs())) + return; + + // Autosave interval reached, only save if there are unsaved messages. + if (g_messageStoreHasUnsavedChanges) { + LOG_INFO("Autosaving MessageStore to flash"); + store->saveToFlash(); + } else { + LOG_INFO("Autosave skipped, no changes to save"); + g_lastAutoSaveMs = now; + } +} +#endif + +// Add from incoming/outgoing packet +const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet) +{ + StoredMessage sm; + assignTimestamp(sm); + sm.channelIndex = packet.channel; + + const char *payload = reinterpret_cast(packet.decoded.payload.bytes); + size_t len = strnlen(payload, MAX_MESSAGE_SIZE - 1); + sm.textOffset = storeTextInPool(payload, len); + sm.textLength = len; + + // Determine sender + uint32_t localNode = nodeDB->getNodeNum(); + sm.sender = (packet.from == 0) ? localNode : packet.from; + + sm.dest = packet.to; + + bool isDM = (sm.dest != 0 && sm.dest != NODENUM_BROADCAST); + + if (packet.from == 0) { + sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST; + sm.ackStatus = AckStatus::NONE; + } else { + sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST; + sm.ackStatus = AckStatus::ACKED; + } + + addLiveMessage(sm); + +#if ENABLE_MESSAGE_PERSISTENCE + markMessageStoreUnsaved(); +#endif + + return liveMessages.back(); +} + +// Outgoing/manual message +void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text) +{ + StoredMessage sm; + + // Always use our local time (helper handles RTC vs boot time) + assignTimestamp(sm); + + sm.sender = sender; + sm.channelIndex = channelIndex; + sm.textOffset = storeTextInPool(text.c_str(), text.size()); + sm.textLength = text.size(); + + // Use the provided destination + sm.dest = sender; + sm.type = MessageType::DM_TO_US; + + // Outgoing messages always start with unknown ack status + sm.ackStatus = AckStatus::NONE; + + addLiveMessage(sm); + +#if ENABLE_MESSAGE_PERSISTENCE + markMessageStoreUnsaved(); +#endif +} + +#if ENABLE_MESSAGE_PERSISTENCE + +// Compact, fixed-size on-flash representation using offset + length +struct __attribute__((packed)) StoredMessageRecord { + uint32_t timestamp; + uint32_t sender; + uint8_t channelIndex; + uint32_t dest; + uint8_t isBootRelative; + uint8_t ackStatus; // static_cast(AckStatus) + uint8_t type; // static_cast(MessageType) + uint16_t textLength; // message length + char text[MAX_MESSAGE_SIZE]; // store actual text here +}; + +// Serialize one StoredMessage to flash +static inline void writeMessageRecord(SafeFile &f, const StoredMessage &m) +{ + StoredMessageRecord rec = {}; + rec.timestamp = m.timestamp; + rec.sender = m.sender; + rec.channelIndex = m.channelIndex; + rec.dest = m.dest; + rec.isBootRelative = m.isBootRelative; + rec.ackStatus = static_cast(m.ackStatus); + rec.type = static_cast(m.type); + rec.textLength = m.textLength; + + // Copy the actual text into the record from RAM pool + const char *txt = getTextFromPool(m.textOffset); + strncpy(rec.text, txt, MAX_MESSAGE_SIZE - 1); + rec.text[MAX_MESSAGE_SIZE - 1] = '\0'; + + f.write(reinterpret_cast(&rec), sizeof(rec)); +} + +// Deserialize one StoredMessage from flash; returns false on short read +static inline bool readMessageRecord(File &f, StoredMessage &m) +{ + StoredMessageRecord rec = {}; + if (f.readBytes(reinterpret_cast(&rec), sizeof(rec)) != sizeof(rec)) + return false; + + m.timestamp = rec.timestamp; + m.sender = rec.sender; + m.channelIndex = rec.channelIndex; + m.dest = rec.dest; + m.isBootRelative = rec.isBootRelative; + m.ackStatus = static_cast(rec.ackStatus); + m.type = static_cast(rec.type); + m.textLength = rec.textLength; + + // 💡 Re-store text into pool and update offset + m.textLength = strnlen(rec.text, MAX_MESSAGE_SIZE - 1); + m.textOffset = storeTextInPool(rec.text, m.textLength); + + return true; +} + +void MessageStore::saveToFlash() +{ +#ifdef FSCom + // Ensure root exists + spiLock->lock(); + FSCom.mkdir("/"); + spiLock->unlock(); + + SafeFile f(filename.c_str(), false); + + spiLock->lock(); + uint8_t count = static_cast(liveMessages.size()); + if (count > MAX_MESSAGES_SAVED) + count = MAX_MESSAGES_SAVED; + f.write(&count, 1); + + for (uint8_t i = 0; i < count; ++i) { + writeMessageRecord(f, liveMessages[i]); + } + spiLock->unlock(); + + f.close(); +#endif + + // Reset autosave state after any save + g_messageStoreHasUnsavedChanges = false; + g_lastAutoSaveMs = millis(); +} + +void MessageStore::loadFromFlash() +{ + std::deque().swap(liveMessages); + resetMessagePool(); // reset pool when loading + +#ifdef FSCom + concurrency::LockGuard guard(spiLock); + + if (!FSCom.exists(filename.c_str())) + return; + + auto f = FSCom.open(filename.c_str(), FILE_O_READ); + if (!f) + return; + + uint8_t count = 0; + f.readBytes(reinterpret_cast(&count), 1); + if (count > MAX_MESSAGES_SAVED) + count = MAX_MESSAGES_SAVED; + + for (uint8_t i = 0; i < count; ++i) { + StoredMessage m; + if (!readMessageRecord(f, m)) + break; + liveMessages.push_back(m); + } + + f.close(); +#endif + // Loading messages does not trigger an autosave + g_messageStoreHasUnsavedChanges = false; + g_lastAutoSaveMs = millis(); +} + +#else +// If persistence is disabled, these functions become no-ops +void MessageStore::saveToFlash() {} +void MessageStore::loadFromFlash() {} +#endif + +// Clear all messages (RAM + persisted queue) +void MessageStore::clearAllMessages() +{ + std::deque().swap(liveMessages); + resetMessagePool(); + +#ifdef FSCom + SafeFile f(filename.c_str(), false); + uint8_t count = 0; + f.write(&count, 1); // write "0 messages" + f.close(); +#endif + +#if ENABLE_MESSAGE_PERSISTENCE + g_messageStoreHasUnsavedChanges = false; + g_lastAutoSaveMs = millis(); +#endif +} + +// Internal helper: erase first or last message matching a predicate +template static void eraseIf(std::deque &deque, Predicate pred, bool fromBack = false) +{ + if (fromBack) { + // Iterate from the back and erase all matches from the end + for (auto it = deque.rbegin(); it != deque.rend();) { + if (pred(*it)) { + it = std::deque::reverse_iterator(deque.erase(std::next(it).base())); + } else { + ++it; + } + } + } else { + // Manual forward search to erase all matches + for (auto it = deque.begin(); it != deque.end();) { + if (pred(*it)) { + it = deque.erase(it); + } else { + ++it; + } + } + } +} + +// Delete oldest message (RAM + persisted queue) +void MessageStore::deleteOldestMessage() +{ + eraseIf(liveMessages, [](StoredMessage &) { return true; }); + saveToFlash(); +} + +// Delete oldest message in a specific channel +void MessageStore::deleteOldestMessageInChannel(uint8_t channel) +{ + auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; }; + eraseIf(liveMessages, pred); + saveToFlash(); +} + +void MessageStore::deleteAllMessagesInChannel(uint8_t channel) +{ + auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; }; + eraseIf(liveMessages, pred, false /* delete ALL, not just first */); + saveToFlash(); +} + +void MessageStore::deleteAllMessagesWithPeer(uint32_t peer) +{ + uint32_t local = nodeDB->getNodeNum(); + auto pred = [&](const StoredMessage &m) { + if (m.type != MessageType::DM_TO_US) + return false; + uint32_t other = (m.sender == local) ? m.dest : m.sender; + return other == peer; + }; + eraseIf(liveMessages, pred, false); + saveToFlash(); +} + +// Delete oldest message in a direct chat with a node +void MessageStore::deleteOldestMessageWithPeer(uint32_t peer) +{ + auto pred = [peer](const StoredMessage &m) { + if (m.type != MessageType::DM_TO_US) + return false; + uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; + return other == peer; + }; + eraseIf(liveMessages, pred); + saveToFlash(); +} + +std::deque MessageStore::getChannelMessages(uint8_t channel) const +{ + std::deque result; + for (const auto &m : liveMessages) { + if (m.type == MessageType::BROADCAST && m.channelIndex == channel) { + result.push_back(m); + } + } + return result; +} + +std::deque MessageStore::getDirectMessages() const +{ + std::deque result; + for (const auto &m : liveMessages) { + if (m.type == MessageType::DM_TO_US) { + result.push_back(m); + } + } + return result; +} + +// Upgrade boot-relative timestamps once RTC is valid +// Only same-boot boot-relative messages are healed. +// Persisted boot-relative messages from old boots stay ??? forever. +void MessageStore::upgradeBootRelativeTimestamps() +{ + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + if (nowSecs == 0) + return; // Still no valid RTC + + uint32_t bootNow = millis() / 1000; + + auto fix = [&](std::deque &dq) { + for (auto &m : dq) { + if (m.isBootRelative && m.timestamp <= bootNow) { + uint32_t bootOffset = nowSecs - bootNow; + m.timestamp += bootOffset; + m.isBootRelative = false; + } + } + }; + fix(liveMessages); +} + +const char *MessageStore::getText(const StoredMessage &msg) +{ + // Wrapper around the internal helper + return getTextFromPool(msg.textOffset); +} + +uint16_t MessageStore::storeText(const char *src, size_t len) +{ + // Wrapper around the internal helper + return storeTextInPool(src, len); +} + +#if ENABLE_MESSAGE_PERSISTENCE +void messageStoreAutosaveTick() +{ + // Called from the main loop to check autosave timing + autosaveTick(&messageStore); +} +#endif + +// Global definition +MessageStore messageStore("default"); +#endif diff --git a/src/MessageStore.h b/src/MessageStore.h new file mode 100644 index 000000000..6203d8ed0 --- /dev/null +++ b/src/MessageStore.h @@ -0,0 +1,136 @@ +#pragma once + +#if HAS_SCREEN + +// Disable debug logging entirely on release builds of HELTEC_MESH_SOLAR for space constraints +#if defined(HELTEC_MESH_SOLAR) +#define LOG_DEBUG(...) +#endif + +// Enable or disable message persistence (flash storage) +// Define -DENABLE_MESSAGE_PERSISTENCE=0 in build_flags to disable it entirely +#ifndef ENABLE_MESSAGE_PERSISTENCE +#define ENABLE_MESSAGE_PERSISTENCE 1 +#endif + +#include "mesh/generated/meshtastic/mesh.pb.h" +#include +#include +#include + +// How many messages are stored (RAM + flash). +// Define -DMESSAGE_HISTORY_LIMIT=N in build_flags to control memory usage. +#ifndef MESSAGE_HISTORY_LIMIT +#define MESSAGE_HISTORY_LIMIT 20 +#endif + +// Internal alias used everywhere in code – do NOT redefine elsewhere. +#define MAX_MESSAGES_SAVED MESSAGE_HISTORY_LIMIT + +// Maximum text payload size per message in bytes. +// This still defines the max message length, but we no longer reserve this space per message. +#define MAX_MESSAGE_SIZE 220 + +// Total shared text pool size for all messages combined. +// The text pool is RAM-only. Text is re-stored from flash into the pool on boot. +#ifndef MESSAGE_TEXT_POOL_SIZE +#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE) +#endif + +// Explicit message classification +enum class MessageType : uint8_t { + BROADCAST = 0, // broadcast message + DM_TO_US = 1 // direct message addressed to this node +}; + +// Delivery status for messages we sent +enum class AckStatus : uint8_t { + NONE = 0, // just sent, waiting (no symbol shown) + ACKED = 1, // got a valid ACK from destination + NACKED = 2, // explicitly failed + TIMEOUT = 3, // no ACK after retry window + RELAYED = 4 // got an ACK from relay, not destination +}; + +struct StoredMessage { + uint32_t timestamp; // When message was created (secs since boot or RTC) + uint32_t sender; // NodeNum of sender + uint8_t channelIndex; // Channel index used + uint32_t dest; // Destination node (broadcast or direct) + MessageType type; // Derived from dest (explicit classification) + bool isBootRelative; // true = millis()/1000 fallback; false = epoch/RTC absolute + AckStatus ackStatus; // Delivery status (only meaningful for our own sent messages) + + // Text storage metadata — rebuilt from flash at boot + uint16_t textOffset; // Offset into global text pool (valid only after loadFromFlash()) + uint16_t textLength; // Length of text in bytes + + // Default constructor initializes all fields safely + StoredMessage() + : timestamp(0), sender(0), channelIndex(0), dest(0xffffffff), type(MessageType::BROADCAST), isBootRelative(false), + ackStatus(AckStatus::NONE), textOffset(0), textLength(0) + { + } +}; + +class MessageStore +{ + public: + explicit MessageStore(const std::string &label); + + // Live RAM methods (always current, used by UI and runtime) + void addLiveMessage(StoredMessage &&msg); + void addLiveMessage(const StoredMessage &msg); // convenience overload + const std::deque &getLiveMessages() const { return liveMessages; } + + // Add new messages from packets or manual input + const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only + void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); // Manual add + + // Persistence methods (used only on boot/shutdown) + void saveToFlash(); // Save messages to flash + void loadFromFlash(); // Load messages from flash + + // Clear all messages (RAM + persisted queue + text pool) + void clearAllMessages(); + + // Delete helpers + void deleteOldestMessage(); // remove oldest from RAM (and flash on save) + void deleteOldestMessageInChannel(uint8_t channel); + void deleteOldestMessageWithPeer(uint32_t peer); + void deleteAllMessagesInChannel(uint8_t channel); + void deleteAllMessagesWithPeer(uint32_t peer); + + // Unified accessor (for UI code, defaults to RAM buffer) + const std::deque &getMessages() const { return liveMessages; } + + // Helper filters for future use + std::deque getChannelMessages(uint8_t channel) const; // Only broadcast messages on a channel + std::deque getDirectMessages() const; // Only direct messages + + // Upgrade boot-relative timestamps once RTC is valid + void upgradeBootRelativeTimestamps(); + + // Retrieve the C-string text for a stored message + static const char *getText(const StoredMessage &msg); + + // Allocate text into pool (used by sender-side code) + static uint16_t storeText(const char *src, size_t len); + + // Used when loading from flash to rebuild the text pool + static uint16_t rebuildTextFromFlash(const char *src, size_t len); + + private: + std::deque liveMessages; // Single in-RAM message buffer (also used for persistence) + std::string filename; // Flash filename for persistence +}; + +#if ENABLE_MESSAGE_PERSISTENCE +// Called periodically from main loop to trigger time based autosave +void messageStoreAutosaveTick(); +#endif + +// Global instance (defined in MessageStore.cpp) +extern MessageStore messageStore; + +#endif diff --git a/src/Power.cpp b/src/Power.cpp index 7bb8896ce..b211d760e 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -1,16 +1,20 @@ /** * @file Power.cpp - * @brief This file contains the implementation of the Power class, which is responsible for managing power-related functionality - * of the device. It includes battery level sensing, power management unit (PMU) control, and power state machine management. The - * Power class is used by the main device class to manage power-related functionality. + * @brief This file contains the implementation of the Power class, which is + * responsible for managing power-related functionality of the device. It + * includes battery level sensing, power management unit (PMU) control, and + * power state machine management. The Power class is used by the main device + * class to manage power-related functionality. * - * The file also includes implementations of various battery level sensors, such as the AnalogBatteryLevel class, which assumes - * the battery voltage is attached via a voltage-divider to an analog input. + * The file also includes implementations of various battery level sensors, such + * as the AnalogBatteryLevel class, which assumes the battery voltage is + * attached via a voltage-divider to an analog input. * * This file is part of the Meshtastic project. * For more information, see: https://meshtastic.org/ */ #include "power.h" +#include "MessageStore.h" #include "NodeDB.h" #include "PowerFSM.h" #include "Throttle.h" @@ -18,6 +22,7 @@ #include "configuration.h" #include "main.h" #include "meshUtils.h" +#include "power/PowerHAL.h" #include "sleep.h" #if defined(ARCH_PORTDUINO) @@ -170,22 +175,12 @@ Power *power; using namespace meshtastic; -#ifndef AREF_VOLTAGE -#if defined(ARCH_NRF52) -/* - * Internal Reference is +/-0.6V, with an adjustable gain of 1/6, 1/5, 1/4, - * 1/3, 1/2 or 1, meaning 3.6, 3.0, 2.4, 1.8, 1.2 or 0.6V for the ADC levels. - * - * External Reference is VDD/4, with an adjustable gain of 1, 2 or 4, meaning - * VDD/4, VDD/2 or VDD for the ADC levels. - * - * Default settings are internal reference with 1/6 gain (GND..3.6V ADC range) - */ -#define AREF_VOLTAGE 3.6 -#else +// NRF52 has AREF_VOLTAGE defined in architecture.h but +// make sure it's included. If something is wrong with NRF52 +// definition - compilation will fail on missing definition +#if !defined(AREF_VOLTAGE) && !defined(ARCH_NRF52) #define AREF_VOLTAGE 3.3 #endif -#endif /** * If this board has a battery level sensor, set this to a valid implementation @@ -232,7 +227,8 @@ static void battery_adcDisable() #endif /** - * A simple battery level sensor that assumes the battery voltage is attached via a voltage-divider to an analog input + * A simple battery level sensor that assumes the battery voltage is attached + * via a voltage-divider to an analog input */ class AnalogBatteryLevel : public HasBatteryLevel { @@ -310,7 +306,8 @@ class AnalogBatteryLevel : public HasBatteryLevel #ifndef BATTERY_SENSE_SAMPLES #define BATTERY_SENSE_SAMPLES \ - 15 // Set the number of samples, it has an effect of increasing sensitivity in complex electromagnetic environment. + 15 // Set the number of samples, it has an effect of increasing sensitivity in + // complex electromagnetic environment. #endif #ifdef BATTERY_PIN @@ -340,7 +337,8 @@ class AnalogBatteryLevel : public HasBatteryLevel battery_adcDisable(); if (!initial_read_done) { - // Flush the smoothing filter with an ADC reading, if the reading is plausibly correct + // Flush the smoothing filter with an ADC reading, if the reading is + // plausibly correct if (scaled > last_read_value) last_read_value = scaled; initial_read_done = true; @@ -349,8 +347,8 @@ class AnalogBatteryLevel : public HasBatteryLevel last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF } - // LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u", BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t) - // (last_read_value)); + // LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u", + // BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t) (last_read_value)); } return last_read_value; #endif // BATTERY_PIN @@ -419,7 +417,8 @@ class AnalogBatteryLevel : public HasBatteryLevel /** * return true if there is a battery installed in this unit */ - // if we have a integrated device with a battery, we can assume that the battery is always connected + // if we have a integrated device with a battery, we can assume that the + // battery is always connected #ifdef BATTERY_IMMUTABLE virtual bool isBatteryConnect() override { return true; } #elif defined(ADC_V) @@ -440,10 +439,10 @@ class AnalogBatteryLevel : public HasBatteryLevel virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; } #endif - /// If we see a battery voltage higher than physics allows - assume charger is pumping - /// in power - /// On some boards we don't have the power management chip (like AXPxxxx) - /// so we use EXT_PWR_DETECT GPIO pin to detect external power source + /// If we see a battery voltage higher than physics allows - assume charger is + /// pumping in power On some boards we don't have the power management chip + /// (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to detect external power + /// source virtual bool isVbusIn() override { #ifdef EXT_PWR_DETECT @@ -460,8 +459,12 @@ class AnalogBatteryLevel : public HasBatteryLevel } // if it's not HIGH - check the battery #endif -#elif defined(MUZI_BASE) - return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk; + +// technically speaking this should work for all(?) NRF52 boards +// but needs testing across multiple devices. NRF52 USB would not even work if +// VBUS was not properly connected and detected by the CPU +#elif defined(MUZI_BASE) || defined(PROMICRO_DIY_TCXO) + return powerHAL_isVBUSConnected(); #endif return getBattVoltage() > chargingVolt; } @@ -475,15 +478,18 @@ class AnalogBatteryLevel : public HasBatteryLevel return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse; } #endif -#ifdef EXT_CHRG_DETECT +#if defined(ELECROW_ThinkNode_M6) + return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value || isVbusIn(); +#elif EXT_CHRG_DETECT return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; #elif defined(BATTERY_CHARGING_INV) return !digitalRead(BATTERY_CHARGING_INV); #else #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION) if (hasINA()) { - // get current flow from INA sensor - negative value means power flowing into the battery - // default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT RESISTOR <--> INA_VIN- <--> LOAD + // get current flow from INA sensor - negative value means power flowing + // into the battery default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT + // RESISTOR <--> INA_VIN- <--> LOAD LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address); #if defined(INA_CHARGING_DETECTION_INVERT) return getINACurrent() > 0; @@ -499,8 +505,8 @@ class AnalogBatteryLevel : public HasBatteryLevel } private: - /// If we see a battery voltage higher than physics allows - assume charger is pumping - /// in power + /// If we see a battery voltage higher than physics allows - assume charger is + /// pumping in power /// For heltecs with no battery connected, the measured voltage is 2204, so // need to be higher than that, in this case is 2500mV (3000-500) @@ -509,7 +515,8 @@ class AnalogBatteryLevel : public HasBatteryLevel const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS; // Start value from minimum voltage for the filter to not start from 0 // that could trigger some events. - // This value is over-written by the first ADC reading, it the voltage seems reasonable. + // This value is over-written by the first ADC reading, it the voltage seems + // reasonable. bool initial_read_done = false; float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS); uint32_t last_read_time_ms = 0; @@ -651,7 +658,8 @@ bool Power::analogInit() #ifdef CONFIG_IDF_TARGET_ESP32S3 // ESP32S3 else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) { - LOG_INFO("ADC config based on Two Point values and fitting curve coefficients stored in eFuse"); + LOG_INFO("ADC config based on Two Point values and fitting curve " + "coefficients stored in eFuse"); } #endif else { @@ -659,13 +667,7 @@ bool Power::analogInit() } #endif // ARCH_ESP32 -#ifdef ARCH_NRF52 -#ifdef VBAT_AR_INTERNAL - analogReference(VBAT_AR_INTERNAL); -#else - analogReference(AR_INTERNAL); // 3.6V -#endif -#endif // ARCH_NRF52 + // NRF52 ADC init moved to powerHAL_init in nrf52 platform #ifndef ARCH_ESP32 analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); @@ -692,6 +694,8 @@ bool Power::setup() found = true; } else if (lipoChargerInit()) { found = true; + } else if (serialBatteryInit()) { + found = true; } else if (meshSolarInit()) { found = true; } else if (analogInit()) { @@ -718,6 +722,16 @@ bool Power::setup() runASAP = true; }, CHANGE); +#endif +#ifdef EXT_CHRG_DETECT + attachInterrupt( + EXT_CHRG_DETECT, + []() { + power->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + }, + CHANGE); #endif enabled = found; low_voltage_counter = 0; @@ -764,7 +778,8 @@ void Power::reboot() HAL_NVIC_SystemReset(); #else rebootAtMsec = -1; - LOG_WARN("FIXME implement reboot for this platform. Note that some settings require a restart to be applied"); + LOG_WARN("FIXME implement reboot for this platform. Note that some settings " + "require a restart to be applied"); #endif } @@ -774,9 +789,12 @@ void Power::shutdown() #if HAS_SCREEN if (screen) { #ifdef T_DECK_PRO - screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button + screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", + 0); // T-Deck Pro has no power button #elif defined(USE_EINK) - screen->showSimpleBanner("Shutting Down...", 2250); // dismiss after 3 seconds to avoid the banner on the sleep screen + screen->showSimpleBanner("Shutting Down...", + 2250); // dismiss after 3 seconds to avoid the + // banner on the sleep screen #else screen->showSimpleBanner("Shutting Down...", 0); // stays on screen #endif @@ -786,7 +804,9 @@ void Power::shutdown() playShutdownMelody(); #endif nodeDB->saveToDisk(); - +#if HAS_SCREEN + messageStore.saveToFlash(); +#endif #if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) #ifdef PIN_LED1 ledOff(PIN_LED1); @@ -796,6 +816,9 @@ void Power::shutdown() #endif #ifdef PIN_LED3 ledOff(PIN_LED3); +#endif +#ifdef LED_NOTIFICATION + ledOff(LED_NOTIFICATION); #endif doDeepSleep(DELAY_FOREVER, true, true); #elif defined(ARCH_PORTDUINO) @@ -813,7 +836,8 @@ void Power::readPowerStatus() int32_t batteryVoltageMv = -1; // Assume unknown int8_t batteryChargePercent = -1; OptionalBool usbPowered = OptUnknown; - OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM code doesn't run every time + OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM + // code doesn't run every time OptionalBool isChargingNow = OptUnknown; if (batteryLevel) { @@ -826,9 +850,10 @@ void Power::readPowerStatus() if (batteryLevel->getBatteryPercent() >= 0) { batteryChargePercent = batteryLevel->getBatteryPercent(); } else { - // If the AXP192 returns a percentage less than 0, the feature is either not supported or there is an error - // In that case, we compute an estimate of the charge percent based on open circuit voltage table defined - // in power.h + // If the AXP192 returns a percentage less than 0, the feature is either + // not supported or there is an error In that case, we compute an + // estimate of the charge percent based on open circuit voltage table + // defined in power.h batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) / ((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))), 0, 100); @@ -836,12 +861,12 @@ void Power::readPowerStatus() } } -// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way better instead to make a Nrf52IsUsbPowered subclass -// (which shares a superclass with the BatteryLevel stuff) -// that just provides a few methods. But in the interest of fixing this bug I'm going to follow current -// practice. -#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates the power states. Takes 20 seconds or so to detect - // changes. +// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way +// better instead to make a Nrf52IsUsbPowered subclass (which shares a +// superclass with the BatteryLevel stuff) that just provides a few methods. But +// in the interest of fixing this bug I'm going to follow current practice. +#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates + // the power states. Takes 20 seconds or so to detect changes. nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get(); // LOG_DEBUG("NRF Power %d", nrf_usb_state); @@ -915,8 +940,9 @@ void Power::readPowerStatus() #endif - // If we have a battery at all and it is less than 0%, force deep sleep if we have more than 10 low readings in - // a row. NOTE: min LiIon/LiPo voltage is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough. + // If we have a battery at all and it is less than 0%, force deep sleep if we + // have more than 10 low readings in a row. NOTE: min LiIon/LiPo voltage + // is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough. // if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) { @@ -938,8 +964,8 @@ int32_t Power::runOnce() readPowerStatus(); #ifdef HAS_PMU - // WE no longer use the IRQ line to wake the CPU (due to false wakes from sleep), but we do poll - // the IRQ status by reading the registers over I2C + // WE no longer use the IRQ line to wake the CPU (due to false wakes from + // sleep), but we do poll the IRQ status by reading the registers over I2C if (PMU) { PMU->getIrqStatus(); @@ -981,7 +1007,8 @@ int32_t Power::runOnce() PMU->clearIrqStatus(); } #endif - // Only read once every 20 seconds once the power status for the app has been initialized + // Only read once every 20 seconds once the power status for the app has been + // initialized return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME; } @@ -989,10 +1016,12 @@ int32_t Power::runOnce() * Init the power manager chip * * axp192 power - DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose comms to the axp192 because the OLED and the - axp192 share the same i2c bus, instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> ESP32 (keep this - on!) LDO1 30mA -> charges GPS backup battery // charges the tiny J13 battery by the GPS to power the GPS ram (for a couple of - days), can not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS + DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose + comms to the axp192 because the OLED and the axp192 share the same i2c bus, + instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> + ESP32 (keep this on!) LDO1 30mA -> charges GPS backup battery // charges the + tiny J13 battery by the GPS to power the GPS ram (for a couple of days), can + not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS * */ bool Power::axpChipInit() @@ -1037,9 +1066,10 @@ bool Power::axpChipInit() if (!PMU) { /* - * In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will be called at the same time. - * In order not to affect other devices, if the initialization of the PMU fails, Wire needs to be re-initialized once, - * if there are multiple devices sharing the bus. + * In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will + * be called at the same time. In order not to affect other devices, if the + * initialization of the PMU fails, Wire needs to be re-initialized once, if + * there are multiple devices sharing the bus. * * */ #ifndef PMU_USE_WIRE1 w->begin(I2C_SDA, I2C_SCL); @@ -1056,8 +1086,8 @@ bool Power::axpChipInit() PMU->enablePowerOutput(XPOWERS_LDO2); // oled module power channel, - // disable it will cause abnormal communication between boot and AXP power supply, - // do not turn it off + // disable it will cause abnormal communication between boot and AXP power + // supply, do not turn it off PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300); // enable oled power PMU->enablePowerOutput(XPOWERS_DCDC1); @@ -1084,7 +1114,8 @@ bool Power::axpChipInit() PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2); } else if (PMU->getChipModel() == XPOWERS_AXP2101) { - /*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it uses an AXP2101 power chip*/ + /*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it + * uses an AXP2101 power chip*/ if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { // Unuse power channel PMU->disablePowerOutput(XPOWERS_DCDC2); @@ -1119,8 +1150,8 @@ bool Power::axpChipInit() // t-beam s3 core /** * gnss module power channel - * The default ALDO4 is off, you need to turn on the GNSS power first, otherwise it will be invalid during - * initialization + * The default ALDO4 is off, you need to turn on the GNSS power first, + * otherwise it will be invalid during initialization */ PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300); PMU->enablePowerOutput(XPOWERS_ALDO4); @@ -1146,11 +1177,11 @@ bool Power::axpChipInit() PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300); PMU->enablePowerOutput(XPOWERS_ALDO1); - // sdcard power channel + // sdcard (T-Beam S3) / gnns (T-Watch S3 Plus) power channel PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); +#ifndef T_WATCH_S3 PMU->enablePowerOutput(XPOWERS_BLDO1); - -#ifdef T_WATCH_S3 +#else // DRV2605 power channel PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300); PMU->enablePowerOutput(XPOWERS_BLDO2); @@ -1170,7 +1201,8 @@ bool Power::axpChipInit() // disable all axp chip interrupt PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ); - // Set the constant current charging current of AXP2101, temporarily use 500mA by default + // Set the constant current charging current of AXP2101, temporarily use + // 500mA by default PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA); // Set up the charging voltage @@ -1236,11 +1268,12 @@ bool Power::axpChipInit() PMU->getPowerChannelVoltage(XPOWERS_BLDO2)); } -// We can safely ignore this approach for most (or all) boards because MCU turned off -// earlier than battery discharged to 2.6V. +// We can safely ignore this approach for most (or all) boards because MCU +// turned off earlier than battery discharged to 2.6V. // -// Unfortanly for now we can't use this killswitch for RAK4630-based boards because they have a bug with -// battery voltage measurement. Probably it sometimes drops to low values. +// Unfortunately for now we can't use this killswitch for RAK4630-based boards +// because they have a bug with battery voltage measurement. Probably it +// sometimes drops to low values. #ifndef RAK4630 // Set PMU shutdown voltage at 2.6V to maximize battery utilization PMU->setSysPowerDownVoltage(2600); @@ -1259,10 +1292,12 @@ bool Power::axpChipInit() attachInterrupt( PMU_IRQ, [] { pmu_irq = true; }, FALLING); - // we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ because it occurs repeatedly while there is - // no battery also it could cause inadvertent waking from light sleep just because the battery filled - // we don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while no battery installed - // we don't look at AXPXXX_VBUS_REMOVED_IRQ because we don't have anything hooked to vbus + // we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ + // because it occurs repeatedly while there is no battery also it could cause + // inadvertent waking from light sleep just because the battery filled we + // don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while + // no battery installed we don't look at AXPXXX_VBUS_REMOVED_IRQ because we + // don't have anything hooked to vbus PMU->enableIRQ(pmuIrqMask); PMU->clearIrqStatus(); @@ -1378,8 +1413,8 @@ class LipoCharger : public HasBatteryLevel bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); if (result) { LOG_INFO("PPM BQ25896 init succeeded"); - // Set the minimum operating voltage. Below this voltage, the PPM will protect - // PPM->setSysPowerDownVoltage(3100); + // Set the minimum operating voltage. Below this voltage, the PPM will + // protect PPM->setSysPowerDownVoltage(3100); // Set input current limit, default is 500mA // PPM->setInputCurrentLimit(800); @@ -1402,7 +1437,8 @@ class LipoCharger : public HasBatteryLevel PPM->enableMeasure(); // Turn on charging function - // If there is no battery connected, do not turn on the charging function + // If there is no battery connected, do not turn on the charging + // function PPM->enableCharge(); } else { LOG_WARN("PPM BQ25896 init failed"); @@ -1437,7 +1473,8 @@ class LipoCharger : public HasBatteryLevel virtual int getBatteryPercent() override { return -1; - // return bq->getChargePercent(); // don't use BQ27220 for battery percent, it is not calibrated + // return bq->getChargePercent(); // don't use BQ27220 for battery percent, + // it is not calibrated } /** @@ -1559,10 +1596,143 @@ bool Power::meshSolarInit() #else /** - * The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel + * The meshSolar battery level sensor is unavailable - default to + * AnalogBatteryLevel */ bool Power::meshSolarInit() { return false; } #endif + +#ifdef HAS_SERIAL_BATTERY_LEVEL +#include + +/** + * SerialBatteryLevel class for pulling battery information from a secondary MCU over serial. + */ +class SerialBatteryLevel : public HasBatteryLevel +{ + + public: + /** + * Init the I2C meshSolar battery level sensor + */ + bool runOnce() + { + BatterySerial.begin(4800); + + return true; + } + + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override { return v_percent; } + + /** + * The raw voltage of the battery in millivolts, or NAN if unknown + */ + virtual uint16_t getBattVoltage() override { return voltage * 1000; } + + /** + * return true if there is a battery installed in this unit + */ + virtual bool isBatteryConnect() override + { + // definitely need to gobble up more bytes at once + if (BatterySerial.available() > 5) { + // LOG_WARN("SerialBatteryLevel: %u bytes available", BatterySerial.available()); + while (BatterySerial.available() > 11) { + BatterySerial.read(); // flush old data + } + // LOG_WARN("SerialBatteryLevel: %u bytes now available", BatterySerial.available()); + int tries = 0; + while (BatterySerial.read() != 0xFE) { + tries++; // wait for start byte + if (tries > 10) { + LOG_WARN("SerialBatteryLevel: no start byte found"); + return 1; + } + } + + Data[1] = BatterySerial.read(); + Data[2] = BatterySerial.read(); + Data[3] = BatterySerial.read(); + Data[4] = BatterySerial.read(); + Data[5] = BatterySerial.read(); + if (Data[5] != 0xFD) { + LOG_WARN("SerialBatteryLevel: invalid end byte %02x", Data[5]); + return true; + } + v_percent = Data[1]; + voltage = Data[2] + (((float)Data[3]) / 100) + (((float)Data[4]) / 10000); + voltage *= 2; + // LOG_WARN("SerialBatteryLevel: received data %u, %f, %02x", v_percent, voltage, Data[5]); + return true; + } + // This function runs first, so use it to grab the latest data from the secondary MCU + return true; + } + + /** + * return true if there is an external power source detected + */ + virtual bool isVbusIn() override + { +#if defined(EXT_CHRG_DETECT) + + return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; + +#endif + return false; + } + + virtual bool isCharging() override + { +#ifdef EXT_CHRG_DETECT + return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; + +#endif + // by default, we check the battery voltage only + return isVbusIn(); + } + + private: + SoftwareSerial BatterySerial = SoftwareSerial(SERIAL_BATTERY_RX, SERIAL_BATTERY_TX); + uint8_t Data[6] = {0}; + int v_percent = 0; + float voltage = 0.0; +}; + +SerialBatteryLevel serialBatteryLevel; + +/** + * Init the serial battery level sensor + */ +bool Power::serialBatteryInit() +{ +#ifdef EXT_PWR_DETECT + pinMode(EXT_PWR_DETECT, INPUT); +#endif +#ifdef EXT_CHRG_DETECT + pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode); +#endif + + bool result = serialBatteryLevel.runOnce(); + LOG_DEBUG("Power::serialBatteryInit serial battery sensor is %s", result ? "ready" : "not ready yet"); + if (!result) + return false; + batteryLevel = &serialBatteryLevel; + return true; +} + +#else +/** + * If this device has no serial battery level sensor, don't try to use it. + */ +bool Power::serialBatteryInit() +{ + return false; +} +#endif diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 9624a4593..e15d56912 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -18,7 +18,7 @@ #endif #if HAS_NETWORKING -extern Syslog syslog; +extern meshtastic::Syslog syslog; #endif void RedirectablePrint::rpInit() { @@ -131,6 +131,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, 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 + #ifdef ARCH_PORTDUINO ::printf("%s ", logLevel); if (color) { diff --git a/src/SafeFile.cpp b/src/SafeFile.cpp index 45b96ad07..39436f18e 100644 --- a/src/SafeFile.cpp +++ b/src/SafeFile.cpp @@ -54,7 +54,7 @@ size_t SafeFile::write(const uint8_t *buffer, size_t size) } /** - * Atomically close the file (deleting any old versions) and readback the contents to confirm the hash matches + * Atomically close the file (overwriting any old version) and readback the contents to confirm the hash matches * * @return false for failure */ @@ -73,15 +73,7 @@ bool SafeFile::close() if (!testReadback()) return false; - { // Scope for lock - concurrency::LockGuard g(spiLock); - // brief window of risk here ;-) - if (fullAtomic && FSCom.exists(filename.c_str()) && !FSCom.remove(filename.c_str())) { - LOG_ERROR("Can't remove old pref file"); - return false; - } - } - + // Rename or overwrite (atomic operation) String filenameTmp = filename; filenameTmp += ".tmp"; if (!renameFile(filenameTmp.c_str(), filename.c_str())) { diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp index 7de6c0740..6bb4ef141 100644 --- a/src/buzz/BuzzerFeedbackThread.cpp +++ b/src/buzz/BuzzerFeedbackThread.cpp @@ -22,12 +22,19 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) // Handle different input events with appropriate buzzer feedback switch (event->inputEvent) { +#ifdef INPUTDRIVER_ENCODER_TYPE + case INPUT_BROKER_SELECT: + case INPUT_BROKER_SELECT_LONG: + playClick(); + break; +#else case INPUT_BROKER_USER_PRESS: case INPUT_BROKER_ALT_PRESS: case INPUT_BROKER_SELECT: case INPUT_BROKER_SELECT_LONG: - playBeep(); // Confirmation feedback + playBeep(); break; +#endif case INPUT_BROKER_UP: case INPUT_BROKER_UP_LONG: @@ -58,4 +65,4 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) } return 0; // Allow other handlers to process the event -} \ No newline at end of file +} diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index aa8346585..6fb28a6ac 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -35,6 +35,14 @@ struct ToneDuration { #define NOTE_G6 1568 #define NOTE_E7 2637 +#define NOTE_C4 262 +#define NOTE_E4 330 +#define NOTE_G4 392 +#define NOTE_A4 440 +#define NOTE_C5 523 +#define NOTE_E5 659 +#define NOTE_G5 784 + const int DURATION_1_16 = 62; // 1/16 note const int DURATION_1_8 = 125; // 1/8 note const int DURATION_1_4 = 250; // 1/4 note @@ -65,7 +73,7 @@ void playTones(const ToneDuration *tone_durations, int size) void playBeep() { - ToneDuration melody[] = {{NOTE_B3, DURATION_1_8}}; + ToneDuration melody[] = {{NOTE_B3, DURATION_1_16}}; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } @@ -113,7 +121,14 @@ void playShutdownMelody() void playChirp() { // A short, friendly "chirp" sound for key presses - ToneDuration melody[] = {{NOTE_AS3, 20}}; // Very short AS3 note + ToneDuration melody[] = {{NOTE_AS3, 20}}; // Short AS3 note + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void playClick() +{ + // A very short "click" sound with minimum delay; ideal for rotary encoder events + ToneDuration melody[] = {{NOTE_AS3, 1}}; // Very Short AS3 playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } @@ -182,3 +197,17 @@ void playComboTune() }; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } + +void play4ClickDown() +{ + ToneDuration melody[] = {{NOTE_G5, 55}, {NOTE_E5, 55}, {NOTE_C5, 60}, {NOTE_A4, 55}, {NOTE_G4, 55}, + {NOTE_E4, 65}, {NOTE_C4, 80}, {NOTE_G3, 120}, {NOTE_E3, 160}, {NOTE_SILENT, 120}}; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void play4ClickUp() +{ + // Quick high-pitched notes with trills + ToneDuration melody[] = {{NOTE_F5, 50}, {NOTE_G6, 45}, {NOTE_E7, 60}}; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} \ No newline at end of file diff --git a/src/buzz/buzz.h b/src/buzz/buzz.h index c25a54a5b..1b97e24de 100644 --- a/src/buzz/buzz.h +++ b/src/buzz/buzz.h @@ -7,8 +7,11 @@ void playShutdownMelody(); void playGPSEnableBeep(); void playGPSDisableBeep(); void playComboTune(); +void play4ClickDown(); +void play4ClickUp(); void playBoop(); void playChirp(); +void playClick(); 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/configuration.h b/src/configuration.h index b4ab57053..66fa4492d 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -29,8 +29,8 @@ along with this program. If not, see . #if __has_include("Melopero_RV3028.h") #include "Melopero_RV3028.h" #endif -#if __has_include("pcf8563.h") -#include "pcf8563.h" +#if __has_include("SensorRtcHelper.hpp") +#include "SensorRtcHelper.hpp" #endif /* Offer chance for variant-specific defines */ @@ -155,6 +155,10 @@ along with this program. If not, see . #endif // Default system gain to 0 if not defined +#ifndef NUM_PA_POINTS +#define NUM_PA_POINTS 1 +#endif + #ifndef TX_GAIN_LORA #define TX_GAIN_LORA 0 #endif @@ -172,11 +176,12 @@ along with this program. If not, see . // ----------------------------------------------------------------------------- // OLED & Input // ----------------------------------------------------------------------------- +#define SSD1306_ADDRESS_L 0x3C // Addr = 0 +#define SSD1306_ADDRESS_H 0x3D // Addr = 1 + #if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) -#define SSD1306_ADDRESS 0x3D +#define SSD1306_ADDRESS SSD1306_ADDRESS_H #define USE_SH1106 -#else -#define SSD1306_ADDRESS 0x3C #endif #define ST7567_ADDRESS 0x3F @@ -205,7 +210,7 @@ along with this program. If not, see . #define INA_ADDR_WAVESHARE_UPS 0x43 #define INA3221_ADDR 0x42 #define MAX1704X_ADDR 0x36 -#define QMC6310_ADDR 0x1C +#define QMC6310U_ADDR 0x1C #define QMI8658_ADDR 0x6B #define QMC5883L_ADDR 0x0D #define HMC5883L_ADDR 0x1E @@ -214,7 +219,7 @@ along with this program. If not, see . #define LPS22HB_ADDR_ALT 0x5D #define SHT31_4x_ADDR 0x44 #define SHT31_4x_ADDR_ALT 0x45 -#define PMSA0031_ADDR 0x12 +#define PMSA003I_ADDR 0x12 #define QMA6100P_ADDR 0x12 #define AHT10_ADDR 0x38 #define RCWL9620_ADDR 0x57 @@ -385,9 +390,6 @@ along with this program. If not, see . #ifndef HAS_RADIO #define HAS_RADIO 0 #endif -#ifndef HAS_RTC -#define HAS_RTC 0 -#endif #ifndef HAS_CPU_SHUTDOWN #define HAS_CPU_SHUTDOWN 0 #endif @@ -423,12 +425,16 @@ along with this program. If not, see . #define HAS_RGB_LED #endif -#ifndef LED_STATE_OFF -#define LED_STATE_OFF 0 -#endif #ifndef LED_STATE_ON #define LED_STATE_ON 1 #endif +#ifndef LED_STATE_OFF +#define LED_STATE_OFF (LED_STATE_ON ^ 1) +#endif + +#ifndef ledOff +#define ledOff(pin) pinMode(pin, INPUT) +#endif // default mapping of pins #if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN) @@ -468,6 +474,7 @@ along with this program. If not, see . #define MESHTASTIC_EXCLUDE_AUDIO 1 #define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1 #define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1 +#define MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR 1 #define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 1 #define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1 #define MESHTASTIC_EXCLUDE_PAXCOUNTER 1 diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index 8ac503b83..4795d2abc 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -25,8 +25,8 @@ ScanI2C::FoundDevice ScanI2C::firstScreen() const ScanI2C::FoundDevice ScanI2C::firstRTC() const { - ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_RX8130CE}; - return firstOfOrNONE(3, types); + ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_PCF85063, RTC_RX8130CE}; + return firstOfOrNONE(4, types); } ScanI2C::FoundDevice ScanI2C::firstKeyboard() const @@ -43,7 +43,7 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const ScanI2C::FoundDevice ScanI2C::firstAQI() const { - ScanI2C::DeviceType types[] = {PMSA0031, SCD4X}; + ScanI2C::DeviceType types[] = {PMSA003I, SCD4X}; return firstOfOrNONE(2, types); } diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index cced980a6..dffcd8fb6 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -14,6 +14,7 @@ class ScanI2C SCREEN_ST7567, RTC_RV3028, RTC_PCF8563, + RTC_PCF85063, RTC_RX8130CE, CARDKB, TDECKKB, @@ -34,11 +35,12 @@ class ScanI2C SHT4X, SHTC3, LPS22HB, - QMC6310, + QMC6310U, + QMC6310N, QMI8658, QMC5883L, HMC5883L, - PMSA0031, + PMSA003I, QMA6100P, MPU6050, LIS3DH, diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index db269ac64..c6ef34846 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -63,12 +63,16 @@ ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const if (i2cBus->available()) { r = i2cBus->read(); } + if (r == 0x80) { + LOG_INFO("QMC6310N found at address 0x%02X", addr.address); + return ScanI2C::DeviceType::QMC6310N; + } r &= 0x0f; if (r == 0x08 || r == 0x00) { logFoundDevice("SH1106", (uint8_t)addr.address); o_probe = SCREEN_SH1106; // SH1106 - } else if (r == 0x03 || r == 0x04 || r == 0x06 || r == 0x07) { + } else if (r == 0x03 || r == 0x04 || r == 0x06 || r == 0x07 || r == 0x05) { logFoundDevice("SSD1306", (uint8_t)addr.address); o_probe = SCREEN_SSD1306; // SSD1306 } @@ -106,7 +110,7 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation if (i2cBus->available()) i2cBus->read(); } - LOG_DEBUG("Register value: 0x%x", value); + LOG_DEBUG("Register value from 0x%x: 0x%x", registerLocation.i2cAddress.address, value); return value; } @@ -175,7 +179,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = NONE; if (err == 0) { switch (addr.address) { - case SSD1306_ADDRESS: + case SSD1306_ADDRESS_H: + case SSD1306_ADDRESS_L: type = probeOLED(addr); break; @@ -202,6 +207,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address) #endif +#ifdef PCF85063_RTC + SCAN_SIMPLE_CASE(PCF85063_RTC, RTC_PCF85063, "PCF85063", (uint8_t)addr.address) +#endif + case CARDKB_ADDR: // Do we have the RAK14006 instead? registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1); @@ -378,11 +387,11 @@ 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, 0x7E), 2); - if (registerValue == 0x5449) { + if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) { type = OPT3001; logFoundDevice("OPT3001", (uint8_t)addr.address); - } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2) != 0) { // unique SHT4x serial number + } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) != + 0) { // unique SHT4x serial number (6 bytes inc. CRC) type = SHT4X; logFoundDevice("SHT4X", (uint8_t)addr.address); } else { @@ -408,7 +417,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) case LPS22HB_ADDR_ALT: SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address) - SCAN_SIMPLE_CASE(QMC6310_ADDR, QMC6310, "QMC6310", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address) case QMI8658_ADDR: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID @@ -438,7 +447,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) #ifdef HAS_QMA6100P SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address) #else - SCAN_SIMPLE_CASE(PMSA0031_ADDR, PMSA0031, "PMSA0031", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(PMSA003I_ADDR, PMSA003I, "PMSA003I", (uint8_t)addr.address) #endif case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); @@ -483,7 +492,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) } break; case TSL25911_ADDR: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x12), 1); + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xA0 | 0x12), 1); if (registerValue == 0x50) { type = TSL2591; logFoundDevice("TSL25911", (uint8_t)addr.address); diff --git a/src/detect/reClockI2C.h b/src/detect/reClockI2C.h new file mode 100644 index 000000000..689e88d6f --- /dev/null +++ b/src/detect/reClockI2C.h @@ -0,0 +1,41 @@ +#ifdef CAN_RECLOCK_I2C +#include "ScanI2CTwoWire.h" + +uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus) +{ + + uint32_t currentClock; + + /* See https://github.com/arduino/Arduino/issues/11457 + Currently, only ESP32 can getClock() + While all cores can setClock() + https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50 + https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60 + https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103 + For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes) + we need to reclock I2C and set it back to the previous desired speed. + Only for cases where we can know OR predefine the speed, we can do this. + */ + +#ifdef ARCH_ESP32 + currentClock = i2cBus->getClock(); +#elif defined(ARCH_NRF52) + // TODO add getClock function or return a predefined clock speed per variant? + return 0; +#elif defined(ARCH_RP2040) + // TODO add getClock function or return a predefined clock speed per variant + return 0; +#elif defined(ARCH_STM32WL) + // TODO add getClock function or return a predefined clock speed per variant + return 0; +#else + return 0; +#endif + + if (currentClock != desiredClock) { + LOG_DEBUG("Changing I2C clock to %u", desiredClock); + i2cBus->setClock(desiredClock); + } + return currentClock; +} +#endif diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index a61a71dde..13e5c32d1 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -896,18 +896,21 @@ void GPS::writePinEN(bool on) void GPS::writePinStandby(bool standby) { #ifdef PIN_GPS_STANDBY // Specifically the standby pin for L76B, L76K and clones - -// Determine the new value for the pin -// Normally: active HIGH for awake -#ifdef PIN_GPS_STANDBY_INVERTED - bool val = standby; -#else - bool val = !standby; -#endif + bool val; + if (standby) + val = GPS_STANDBY_ACTIVE; + else + val = !GPS_STANDBY_ACTIVE; // Write and log pinMode(PIN_GPS_STANDBY, OUTPUT); digitalWrite(PIN_GPS_STANDBY, val); + + // Enter backup mode on PA1010D; TODO: may be applicable to other MTK GPS too + if (IS_ONE_OF(gnssModel, GNSS_MODEL_MTK_PA1010D)) { + _serial_gps->write("$PMTK225,4*2F\r\n"); + } + #ifdef GPS_DEBUG LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW"); #endif @@ -934,8 +937,11 @@ void GPS::setPowerPMU(bool on) // t-beam v1.2 GNSS power channel on ? PMU->enablePowerOutput(XPOWERS_ALDO3) : PMU->disablePowerOutput(XPOWERS_ALDO3); } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE) { - // t-beam-s3-core GNSS power channel + // t-beam-s3-core GNSS power channel on ? PMU->enablePowerOutput(XPOWERS_ALDO4) : PMU->disablePowerOutput(XPOWERS_ALDO4); + } else if (HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) { + // t-watch-s3-plus GNSS power channel + on ? PMU->enablePowerOutput(XPOWERS_BLDO1) : PMU->disablePowerOutput(XPOWERS_BLDO1); } } else if (model == XPOWERS_AXP192) { // t-beam v1.1 GNSS power channel diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 59cee7113..fcbf361d5 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -16,6 +16,11 @@ #define GPS_EN_ACTIVE 1 #endif +// Allow defining the polarity of the STANDBY output. default is LOW for standby +#ifndef GPS_STANDBY_ACTIVE +#define GPS_STANDBY_ACTIVE LOW +#endif + static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL; static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000; diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 1122f0a51..3bca6f6ec 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -66,26 +66,26 @@ RTCSetResult readFromRTC() currentQuality = RTCQualityDevice; } return RTCSetResultSuccess; + } else { + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); } -#elif defined(PCF8563_RTC) +#elif defined(PCF8563_RTC) || defined(PCF85063_RTC) +#if defined(PCF8563_RTC) if (rtc_found.address == PCF8563_RTC) { +#elif defined(PCF85063_RTC) + if (rtc_found.address == PCF85063_RTC) { +#endif uint32_t now = millis(); - PCF8563_Class rtc; + SensorRtcHelper rtc; #if WIRE_INTERFACES_COUNT == 2 rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else - rtc.begin(); + rtc.begin(Wire); #endif - auto tc = rtc.getDateTime(); - tm t; - t.tm_year = tc.year - 1900; - t.tm_mon = tc.month - 1; - t.tm_mday = tc.day; - t.tm_hour = tc.hour; - t.tm_min = tc.minute; - t.tm_sec = tc.second; + RTC_DateTime datetime = rtc.getDateTime(); + tm t = datetime.toUnixTime(); tv.tv_sec = gm_mktime(&t); tv.tv_usec = 0; uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms @@ -100,14 +100,16 @@ RTCSetResult readFromRTC() } #endif - LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, - t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); + LOG_DEBUG("Read RTC time from %s getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900, + t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); if (currentQuality == RTCQualityNone) { timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; } return RTCSetResultSuccess; + } else { + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); } #elif defined(RX8130CE_RTC) if (rtc_found.address == RX8130CE_RTC) { @@ -232,20 +234,28 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd rtc.setTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_wday, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); LOG_DEBUG("RV3028_RTC setTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); + } else { + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); } -#elif defined(PCF8563_RTC) +#elif defined(PCF8563_RTC) || defined(PCF85063_RTC) +#if defined(PCF8563_RTC) if (rtc_found.address == PCF8563_RTC) { - PCF8563_Class rtc; +#elif defined(PCF85063_RTC) + if (rtc_found.address == PCF85063_RTC) { +#endif + SensorRtcHelper rtc; #if WIRE_INTERFACES_COUNT == 2 rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else - rtc.begin(); + rtc.begin(Wire); #endif tm *t = gmtime(&tv->tv_sec); - rtc.setDateTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); - LOG_DEBUG("PCF8563_RTC setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, - t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); + rtc.setDateTime(*t); + LOG_DEBUG("%s setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t->tm_year + 1900, t->tm_mon + 1, + t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); + } else { + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); } #elif defined(RX8130CE_RTC) if (rtc_found.address == RX8130CE_RTC) { @@ -266,11 +276,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd settimeofday(tv, NULL); #endif - // nrf52 doesn't have a readable RTC (yet - software not written) -#if HAS_RTC readFromRTC(); -#endif - return RTCSetResultSuccess; } else { return RTCSetResultNotSet; // RTC was already set with a higher quality time @@ -387,7 +393,7 @@ uint32_t getValidTime(RTCQuality minQuality, bool local) return (currentQuality >= minQuality) ? getTime(local) : 0; } -time_t gm_mktime(struct tm *tm) +time_t gm_mktime(const struct tm *tm) { #if !MESHTASTIC_EXCLUDE_TZ time_t result = 0; @@ -403,8 +409,8 @@ time_t gm_mktime(struct tm *tm) days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400); // Now, within this tm->year, compute the days *before* this tm->month starts. - int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year - int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11 + static const int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year + int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11 // If this is a leap year, and we're past February, add a day: if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) { @@ -425,6 +431,7 @@ time_t gm_mktime(struct tm *tm) return result; #else - return mktime(tm); + struct tm tmCopy = *tm; + return mktime(&tmCopy); #endif } diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 06dd34c16..cf6db0239 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -54,7 +54,7 @@ uint32_t getValidTime(RTCQuality minQuality, bool local = false); RTCSetResult readFromRTC(); -time_t gm_mktime(struct tm *tm); +time_t gm_mktime(const struct tm *tm); #define SEC_PER_DAY 86400 #define SEC_PER_HOUR 3600 diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 4209baf5d..1678da793 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -148,7 +148,7 @@ bool EInkDisplay::connect() #endif #endif -#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) +#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) || defined(TTGO_T_ECHO_PLUS) { auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1); diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index 9975527aa..f5418b069 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -9,6 +9,15 @@ #include "GxEPD2Multi.h" #endif +// Limit how often we push a full E-Ink refresh. T-Deck Pro needs faster updates for typing. +#ifndef EINK_FORCE_DISPLAY_THROTTLE_MS +#if defined(T_DECK_PRO) +#define EINK_FORCE_DISPLAY_THROTTLE_MS 200 +#else +#define EINK_FORCE_DISPLAY_THROTTLE_MS 1000 +#endif +#endif + /** * An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation. * @@ -42,7 +51,7 @@ class EInkDisplay : public OLEDDisplay * * @return true if we did draw the screen */ - virtual bool forceDisplay(uint32_t msecLimit = 1000); + virtual bool forceDisplay(uint32_t msecLimit = EINK_FORCE_DISPLAY_THROTTLE_MS); /** * Run any code needed to complete an update, after the physical refresh has completed. diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 26cffbee4..103428a42 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -46,6 +46,7 @@ along with this program. If not, see . #endif #include "FSCommon.h" #include "MeshService.h" +#include "MessageStore.h" #include "RadioLibInterface.h" #include "error.h" #include "gps/GeoCoord.h" @@ -64,10 +65,7 @@ along with this program. If not, see . #include "modules/WaypointModule.h" #include "sleep.h" #include "target_specific.h" - -using graphics::Emote; -using graphics::emotes; -using graphics::numEmotes; +extern MessageStore messageStore; #if USE_TFTDISPLAY extern uint16_t TFT_MESH; @@ -119,10 +117,6 @@ 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; -// Global variables for screen function overlay symbols -std::vector functionSymbol; -std::string functionSymbolString; - #if HAS_GPS // GeoCoord object for the screen GeoCoord geoCoord; @@ -263,19 +257,11 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int } else { // otherwise, just display the module frame that's aligned with the current frame module_frame = state->currentFrame; - // LOG_DEBUG("Screen is not in transition. Frame: %d", module_frame); } - // LOG_DEBUG("Draw Module Frame %d", module_frame); MeshModule &pi = *moduleFrames.at(module_frame); pi.drawFrame(display, state, x, y); } -// 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; -} - /** * Given a recent lat/lon return a guess of the heading the user is walking on. * @@ -322,17 +308,28 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O { graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; - LOG_INFO("Protobuf Value uiconfig.screen_rgb_color: %d", uiconfig.screen_rgb_color); int32_t rawRGB = uiconfig.screen_rgb_color; - if (rawRGB > 0 && rawRGB <= 255255255) { - uint8_t TFT_MESH_r = (rawRGB >> 16) & 0xFF; - uint8_t TFT_MESH_g = (rawRGB >> 8) & 0xFF; - uint8_t TFT_MESH_b = rawRGB & 0xFF; - LOG_INFO("Values of r,g,b: %d, %d, %d", TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); - if (TFT_MESH_r <= 255 && TFT_MESH_g <= 255 && TFT_MESH_b <= 255) { - TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); + // Only validate the combined value once + if (rawRGB > 0 && rawRGB <= 255255255) { + LOG_INFO("Setting screen RGB color to user chosen: 0x%06X", rawRGB); + // Extract each component as a normal int first + int r = (rawRGB >> 16) & 0xFF; + int g = (rawRGB >> 8) & 0xFF; + int b = rawRGB & 0xFF; + if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { + TFT_MESH = COLOR565(static_cast(r), static_cast(g), static_cast(b)); } +#ifdef TFT_MESH_OVERRIDE + } else if (rawRGB == 0) { + LOG_INFO("Setting screen RGB color to TFT_MESH_OVERRIDE: 0x%04X", TFT_MESH_OVERRIDE); + // Default to TFT_MESH_OVERRIDE if available + TFT_MESH = TFT_MESH_OVERRIDE; +#endif + } else { + // Default best readable yellow color + LOG_INFO("Setting screen RGB color to default: (255,255,128)"); + TFT_MESH = COLOR565(255, 255, 128); } #if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64) @@ -550,10 +547,10 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) void Screen::setup() { - // === Enable display rendering === + // Enable display rendering useDisplay = true; - // === Load saved brightness from UI config === + // Load saved brightness from UI config // For OLED displays (SSD1306), default brightness is 255 if not set if (uiconfig.screen_brightness == 0) { #if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) @@ -565,7 +562,7 @@ void Screen::setup() brightness = uiconfig.screen_brightness; } - // === Detect OLED subtype (if supported by board variant) === + // Detect OLED subtype (if supported by board variant) #ifdef AutoOLEDWire_h if (isAUTOOled) static_cast(dispdev)->setDetected(model); @@ -587,7 +584,7 @@ void Screen::setup() static_cast(dispdev)->setRGB(TFT_MESH); #endif - // === Initialize display and UI system === + // Initialize display and UI system ui->init(); displayWidth = dispdev->width(); displayHeight = dispdev->height(); @@ -599,7 +596,7 @@ void Screen::setup() ui->disableAllIndicators(); // Disable page indicator dots ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance - // === Apply loaded brightness === + // Apply loaded brightness #if defined(ST7789_CS) static_cast(dispdev)->setDisplayBrightness(brightness); #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306) @@ -607,20 +604,20 @@ void Screen::setup() #endif LOG_INFO("Applied screen brightness: %d", brightness); - // === Set custom overlay callbacks === + // Set custom overlay callbacks static OverlayCallback overlays[] = { graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame }; ui->setOverlays(overlays, 1); - // === Enable UTF-8 to display mapping === + // Enable UTF-8 to display mapping dispdev->setFontTableLookupFunction(customFontTableLookup); #ifdef USERPREFS_OEM_TEXT logo_timeout *= 2; // Give more time for branded boot logos #endif - // === Configure alert frames (e.g., "Resuming..." or region name) === + // 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 @@ -636,10 +633,10 @@ void Screen::setup() ui->setFrames(alertFrames, 1); ui->disableAutoTransition(); // Require manual navigation between frames - // === Log buffer for on-screen logs (3 lines max) === + // Log buffer for on-screen logs (3 lines max) dispdev->setLogBuffer(3, 32); - // === Optional screen mirroring or flipping (e.g. for T-Beam orientation) === + // Optional screen mirroring or flipping (e.g. for T-Beam orientation) #ifdef SCREEN_MIRROR dispdev->mirrorScreen(); #else @@ -657,7 +654,7 @@ void Screen::setup() } #endif - // === Generate device ID from MAC address === + // Generate device ID from MAC address uint8_t dmac[6]; getMacAddr(dmac); snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); @@ -666,9 +663,9 @@ void Screen::setup() handleSetOn(false); // Ensure proper init for Arduino targets #endif - // === Turn on display and trigger first draw === + // Turn on display and trigger first draw handleSetOn(true); - determineResolution(dispdev->height(), dispdev->width()); + graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width()); ui->update(); #ifndef USE_EINK ui->update(); // Some SSD1306 clones drop the first draw, so run twice @@ -689,7 +686,7 @@ void Screen::setup() touchScreenImpl1->init(); #endif - // === Subscribe to device status updates === + // Subscribe to device status updates powerStatusObserver.observe(&powerStatus->onNewStatus); gpsStatusObserver.observe(&gpsStatus->onNewStatus); nodeStatusObserver.observe(&nodeStatus->onNewStatus); @@ -697,12 +694,14 @@ void Screen::setup() #if !MESHTASTIC_EXCLUDE_ADMIN adminMessageObserver.observe(adminModule); #endif - if (textMessageModule) - textMessageObserver.observe(textMessageModule); if (inputBroker) inputObserver.observe(inputBroker); - // === Notify modules that support UI events === + // Load persisted messages into RAM + messageStore.loadFromFlash(); + LOG_INFO("MessageStore loaded from flash"); + + // Notify modules that support UI events MeshModule::observeUIEvents(&uiFrameEventObserver); } @@ -773,6 +772,23 @@ int32_t Screen::runOnce() if (displayHeight == 0) { displayHeight = dispdev->getHeight(); } + + // Detect frame transitions and clear message cache when leaving text message screen + { + static int8_t lastFrameIndex = -1; + int8_t currentFrameIndex = ui->getUiState()->currentFrame; + int8_t textMsgIndex = framesetInfo.positions.textMessage; + + if (lastFrameIndex != -1 && currentFrameIndex != lastFrameIndex) { + + if (lastFrameIndex == textMsgIndex && currentFrameIndex != textMsgIndex) { + graphics::MessageRenderer::clearMessageCache(); + } + } + + lastFrameIndex = currentFrameIndex; + } + menuHandler::handleMenuSwitch(dispdev); // Show boot screen for first logo_timeout seconds, then switch to normal operation. @@ -809,7 +825,7 @@ int32_t Screen::runOnce() #endif } #endif - if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { + if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0 && !suppressRebootBanner) { showSimpleBanner("Rebooting...", 0); } @@ -828,17 +844,17 @@ int32_t Screen::runOnce() break; case Cmd::ON_PRESS: if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleOnPress(); + showFrame(FrameDirection::NEXT); } break; case Cmd::SHOW_PREV_FRAME: if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleShowPrevFrame(); + showFrame(FrameDirection::PREVIOUS); } break; case Cmd::SHOW_NEXT_FRAME: if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleShowNextFrame(); + showFrame(FrameDirection::NEXT); } break; case Cmd::START_ALERT_FRAME: { @@ -859,6 +875,7 @@ int32_t Screen::runOnce() break; case Cmd::STOP_ALERT_FRAME: NotificationRenderer::pauseBanner = false; + break; case Cmd::STOP_BOOT_SCREEN: EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { @@ -1029,9 +1046,6 @@ void Screen::setFrames(FrameFocus focus) } #endif - // Declare this early so it’s available in FOCUS_PRESERVE block - bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message); - if (!hiddenFrames.home) { fsi.positions.home = numframes; normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; @@ -1043,11 +1057,16 @@ void Screen::setFrames(FrameFocus focus) indicatorIcons.push_back(icon_mail); #ifndef USE_EINK - if (!hiddenFrames.nodelist) { - fsi.positions.nodelist = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; + if (!hiddenFrames.nodelist_nodes) { + fsi.positions.nodelist_nodes = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes; indicatorIcons.push_back(icon_nodes); } + if (!hiddenFrames.nodelist_location) { + fsi.positions.nodelist_location = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location; + indicatorIcons.push_back(icon_list); + } #endif // Show detailed node views only on E-Ink builds @@ -1069,11 +1088,13 @@ void Screen::setFrames(FrameFocus focus) } #endif #if HAS_GPS +#ifdef USE_EINK if (!hiddenFrames.nodelist_bearings) { fsi.positions.nodelist_bearings = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; indicatorIcons.push_back(icon_list); } +#endif if (!hiddenFrames.gps) { fsi.positions.gps = numframes; normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; @@ -1173,7 +1194,7 @@ void Screen::setFrames(FrameFocus focus) } fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE - this->frameCount = numframes; // ✅ Save frame count for use in custom overlay + this->frameCount = numframes; // Save frame count for use in custom overlay LOG_DEBUG("Finished build frames. numframes: %d", numframes); ui->setFrames(normalFrames, numframes); @@ -1193,10 +1214,6 @@ void Screen::setFrames(FrameFocus focus) 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: // Whichever frame was marked by MeshModule::requestFocus(), if any // If no module requested focus, will show the first frame instead @@ -1239,8 +1256,11 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) void Screen::toggleFrameVisibility(const std::string &frameName) { #ifndef USE_EINK - if (frameName == "nodelist") { - hiddenFrames.nodelist = !hiddenFrames.nodelist; + if (frameName == "nodelist_nodes") { + hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes; + } + if (frameName == "nodelist_location") { + hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location; } #endif #ifdef USE_EINK @@ -1255,9 +1275,11 @@ void Screen::toggleFrameVisibility(const std::string &frameName) } #endif #if HAS_GPS +#ifdef USE_EINK if (frameName == "nodelist_bearings") { hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; } +#endif if (frameName == "gps") { hiddenFrames.gps = !hiddenFrames.gps; } @@ -1279,8 +1301,10 @@ void Screen::toggleFrameVisibility(const std::string &frameName) bool Screen::isFrameHidden(const std::string &frameName) const { #ifndef USE_EINK - if (frameName == "nodelist") - return hiddenFrames.nodelist; + if (frameName == "nodelist_nodes") + return hiddenFrames.nodelist_nodes; + if (frameName == "nodelist_location") + return hiddenFrames.nodelist_location; #endif #ifdef USE_EINK if (frameName == "nodelist_lastheard") @@ -1291,8 +1315,10 @@ bool Screen::isFrameHidden(const std::string &frameName) const return hiddenFrames.nodelist_distance; #endif #if HAS_GPS +#ifdef USE_EINK if (frameName == "nodelist_bearings") return hiddenFrames.nodelist_bearings; +#endif if (frameName == "gps") return hiddenFrames.gps; #endif @@ -1308,37 +1334,6 @@ bool Screen::isFrameHidden(const std::string &frameName) const return false; } -// Dismisses the currently displayed screen frame, if possible -// Relevant for text message, waypoint, others in future? -// Triggered with a CardKB keycombo -void Screen::hideCurrentFrame() -{ - uint8_t currentFrame = ui->getUiState()->currentFrame; - bool dismissed = false; - if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) { - LOG_INFO("Hide Text Message"); - devicestate.has_rx_text_message = false; - memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); - } else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { - LOG_DEBUG("Hide Waypoint"); - devicestate.has_rx_waypoint = false; - hiddenFrames.waypoint = true; - dismissed = true; - } else if (currentFrame == framesetInfo.positions.wifi) { - LOG_DEBUG("Hide WiFi Screen"); - hiddenFrames.wifi = true; - dismissed = true; - } else if (currentFrame == framesetInfo.positions.lora) { - LOG_INFO("Hide LoRa"); - hiddenFrames.lora = true; - dismissed = true; - } - - if (dismissed) { - setFrames(FOCUS_DEFAULT); // You could also use FOCUS_PRESERVE - } -} - void Screen::handleStartFirmwareUpdateScreen() { LOG_DEBUG("Show firmware screen"); @@ -1391,28 +1386,6 @@ void Screen::decreaseBrightness() /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ } -void Screen::setFunctionSymbol(std::string sym) -{ - if (std::find(functionSymbol.begin(), functionSymbol.end(), sym) == functionSymbol.end()) { - functionSymbol.push_back(sym); - functionSymbolString = ""; - for (auto symbol : functionSymbol) { - functionSymbolString = symbol + " " + functionSymbolString; - } - setFastFramerate(); - } -} - -void Screen::removeFunctionSymbol(std::string sym) -{ - functionSymbol.erase(std::remove(functionSymbol.begin(), functionSymbol.end(), sym), functionSymbol.end()); - functionSymbolString = ""; - for (auto symbol : functionSymbol) { - functionSymbolString = symbol + " " + functionSymbolString; - } - setFastFramerate(); -} - void Screen::handleOnPress() { // If screen was off, just wake it, otherwise advance to next frame @@ -1424,23 +1397,17 @@ void Screen::handleOnPress() } } -void Screen::handleShowPrevFrame() +void Screen::showFrame(FrameDirection direction) { - // If screen was off, just wake it, otherwise go back to previous frame - // If we are in a transition, the press must have bounced, drop it. + // Only advance frames when UI is stable if (ui->getUiState()->frameState == FIXED) { - ui->previousFrame(); - lastScreenTransition = millis(); - setFastFramerate(); - } -} -void Screen::handleShowNextFrame() -{ - // 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) { - ui->nextFrame(); + if (direction == FrameDirection::NEXT) { + ui->nextFrame(); + } else { + ui->previousFrame(); + } + lastScreenTransition = millis(); setFastFramerate(); } @@ -1466,7 +1433,6 @@ void Screen::setFastFramerate() int Screen::handleStatusUpdate(const meshtastic::Status *arg) { - // LOG_DEBUG("Screen got status update %d", arg->getStatusType()); switch (arg->getStatusType()) { case STATUS_TYPE_NODE: if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) { @@ -1474,10 +1440,15 @@ int Screen::handleStatusUpdate(const meshtastic::Status *arg) } nodeDB->updateGUI = false; break; - case STATUS_TYPE_POWER: - forceDisplay(true); + case STATUS_TYPE_POWER: { + bool currentUSB = powerStatus->getHasUSB(); + if (currentUSB != lastPowerUSBState) { + lastPowerUSBState = currentUSB; + forceDisplay(true); + } break; } + } return 0; } @@ -1584,11 +1555,11 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) screen->showSimpleBanner(banner, 3000); } else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) { if (longName && longName[0]) { -#if defined(M5STACK_UNITC6L) - strcpy(banner, "New Message"); -#else - snprintf(banner, sizeof(banner), "New Message from\n%s", longName); -#endif + if (currentResolution == ScreenResolution::UltraLow) { + strcpy(banner, "New Message"); + } else { + snprintf(banner, sizeof(banner), "New Message from\n%s", longName); + } } else { strcpy(banner, "New Message"); } @@ -1624,16 +1595,26 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event) if (showingNormalScreen) { // Regenerate the frameset, potentially honoring a module's internal requestFocus() call - if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) + if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) { setFrames(FOCUS_MODULE); + } - // Regenerate the frameset, while Attempt to maintain focus on the current frame - else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) + // Regenerate the frameset, while attempting to maintain focus on the current frame + else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) { setFrames(FOCUS_PRESERVE); + } // Don't regenerate the frameset, just re-draw whatever is on screen ASAP - else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) + else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) { setFastFramerate(); + } + + // Jump directly to the Text Message screen + else if (event->action == UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE) { + setFrames(FOCUS_PRESERVE); // preserve current frame ordering + ui->switchToFrame(framesetInfo.positions.textMessage); + setFastFramerate(); // force redraw ASAP + } } return 0; @@ -1671,7 +1652,48 @@ int Screen::handleInputEvent(const InputEvent *event) menuHandler::handleMenuSwitch(dispdev); return 0; } + // UP/DOWN in message screen scrolls through message threads + if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { + if (event->inputEvent == INPUT_BROKER_UP) { + if (messageStore.getMessages().empty()) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); + } else { + graphics::MessageRenderer::scrollUp(); + setFastFramerate(); // match existing behavior + return 0; + } + } + + if (event->inputEvent == INPUT_BROKER_DOWN) { + if (messageStore.getMessages().empty()) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); + } else { + graphics::MessageRenderer::scrollDown(); + setFastFramerate(); + return 0; + } + } + } + // UP/DOWN in node list screens scrolls through node pages + if (ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { + if (event->inputEvent == INPUT_BROKER_UP) { + graphics::NodeListRenderer::scrollUp(); + setFastFramerate(); + return 0; + } + + if (event->inputEvent == INPUT_BROKER_DOWN) { + graphics::NodeListRenderer::scrollDown(); + setFastFramerate(); + return 0; + } + } // 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 if (showingNormalScreen) { @@ -1685,16 +1707,59 @@ int Screen::handleInputEvent(const InputEvent *event) // If no modules are using the input, move between frames if (!inputIntercepted) { +#if defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2 + bool handledEncoderScroll = false; + const bool isTextMessageFrame = (framesetInfo.positions.textMessage != 255 && + this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage && + !messageStore.getMessages().empty()); + if (isTextMessageFrame) { + if (event->inputEvent == INPUT_BROKER_UP_LONG) { + graphics::MessageRenderer::nudgeScroll(-1); + handledEncoderScroll = true; + } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { + graphics::MessageRenderer::nudgeScroll(1); + handledEncoderScroll = true; + } + } + + if (handledEncoderScroll) { + setFastFramerate(); + return 0; + } +#endif if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { - showPrevFrame(); + showFrame(FrameDirection::PREVIOUS); } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { - showNextFrame(); + showFrame(FrameDirection::NEXT); + } else if (event->inputEvent == INPUT_BROKER_FN_F1) { + this->ui->switchToFrame(0); + lastScreenTransition = millis(); + setFastFramerate(); + } else if (event->inputEvent == INPUT_BROKER_FN_F2) { + this->ui->switchToFrame(1); + lastScreenTransition = millis(); + setFastFramerate(); + } else if (event->inputEvent == INPUT_BROKER_FN_F3) { + this->ui->switchToFrame(2); + lastScreenTransition = millis(); + setFastFramerate(); + } else if (event->inputEvent == INPUT_BROKER_FN_F4) { + this->ui->switchToFrame(3); + lastScreenTransition = millis(); + setFastFramerate(); + } else if (event->inputEvent == INPUT_BROKER_FN_F5) { + this->ui->switchToFrame(4); + lastScreenTransition = millis(); + setFastFramerate(); } else if (event->inputEvent == INPUT_BROKER_UP_LONG) { // Long press up button for fast frame switching showPrevFrame(); } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { // Long press down button for fast frame switching showNextFrame(); + } else if ((event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN) && + this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); } else if (event->inputEvent == INPUT_BROKER_SELECT) { if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { menuHandler::homeBaseMenu(); @@ -1709,20 +1774,21 @@ int Screen::handleInputEvent(const InputEvent *event) } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { menuHandler::loraMenu(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { - if (devicestate.rx_text_message.from) { + if (!messageStore.getMessages().empty()) { menuHandler::messageResponseMenu(); } else { -#if defined(M5STACK_UNITC6L) - menuHandler::textMessageMenu(); -#else - menuHandler::textMessageBaseMenu(); -#endif + if (currentResolution == ScreenResolution::UltraLow) { + menuHandler::textMessageMenu(); + } else { + menuHandler::textMessageBaseMenu(); + } } } else if (framesetInfo.positions.firstFavorite != 255 && this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { menuHandler::favoriteBaseMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist || + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || @@ -1733,7 +1799,7 @@ int Screen::handleInputEvent(const InputEvent *event) menuHandler::wifiBaseMenu(); } } else if (event->inputEvent == INPUT_BROKER_BACK) { - showPrevFrame(); + showFrame(FrameDirection::PREVIOUS); } else if (event->inputEvent == INPUT_BROKER_CANCEL) { setOn(false); } diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index a40579ff5..31ddf1c84 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -40,7 +40,6 @@ class Screen 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 FOCUS_CLOCK, FOCUS_SYSTEM, @@ -55,8 +54,6 @@ class Screen void startFirmwareUpdateScreen() {} void increaseBrightness() {} void decreaseBrightness() {} - void setFunctionSymbol(std::string) {} - void removeFunctionSymbol(std::string) {} void startAlert(const char *) {} void showSimpleBanner(const char *message, uint32_t durationMs = 0) {} void showOverlayBanner(BannerOverlayOptions) {} @@ -172,6 +169,8 @@ class Point namespace graphics { +enum class FrameDirection { NEXT, PREVIOUS }; + // Forward declarations class Screen; @@ -211,8 +210,6 @@ class Screen : public concurrency::OSThread CallbackObserver(this, &Screen::handleStatusUpdate); CallbackObserver nodeStatusObserver = CallbackObserver(this, &Screen::handleStatusUpdate); - CallbackObserver textMessageObserver = - CallbackObserver(this, &Screen::handleTextMessage); CallbackObserver uiFrameEventObserver = CallbackObserver(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules CallbackObserver inputObserver = @@ -223,6 +220,10 @@ class Screen : public concurrency::OSThread public: OLEDDisplay *getDisplayDevice() { return dispdev; } explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); + + // Screen dimension accessors + inline int getHeight() const { return displayHeight; } + inline int getWidth() const { return displayWidth; } size_t frameCount = 0; // Total number of active frames ~Screen(); @@ -231,7 +232,6 @@ class Screen : public concurrency::OSThread 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 FOCUS_CLOCK, FOCUS_SYSTEM, @@ -279,6 +279,7 @@ class Screen : public concurrency::OSThread void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); } void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); } void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); } + void showFrame(FrameDirection direction); // generic alert start void startAlert(FrameCallback _alertFrame) @@ -346,9 +347,6 @@ class Screen : public concurrency::OSThread void increaseBrightness(); void decreaseBrightness(); - void setFunctionSymbol(std::string sym); - void removeFunctionSymbol(std::string sym); - /// Stops showing the boot screen. void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); } @@ -560,6 +558,42 @@ class Screen : public concurrency::OSThread if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5) return (uint8_t)0; +#endif + +#if defined(OLED_GR) + + switch (last) { + case 0xC3: { + SKIPREST = false; + return (uint8_t)(ch | 0xC0); + } + // Map UTF-8 Greek chars to Windows-1253 (CP-1253) ASCII codes + case 0xCE: { + SKIPREST = false; + // Uppercase Greek: Α-Ρ (U+0391-U+03A1) -> CP-1253 193-209 + if (ch >= 145 && ch <= 161) + return (uint8_t)(ch + 48); + // Uppercase Greek: Σ-Ω (U+03A3-U+03A9) -> CP-1253 211-217 + else if (ch >= 163 && ch <= 169) + return (uint8_t)(ch + 48); + // Lowercase Greek: α-ρ (U+03B1-U+03C1) -> CP-1253 225-241 + else if (ch >= 177 && ch <= 193) + return (uint8_t)(ch + 48); + break; + } + case 0xCF: { + SKIPREST = false; + // Lowercase Greek: ς-ω (U+03C2-U+03C9) -> CP-1253 242-249 + if (ch >= 130 && ch <= 137) + return (uint8_t)(ch + 112); + break; + } + } + + // We want to strip out prefix chars for two-byte Greek char formats + if (ch == 0xC2 || ch == 0xC3 || ch == 0xCE || ch == 0xCF) + return (uint8_t)0; + #endif // If we already returned an unconvertable-character symbol for this unconvertable-character sequence, return NULs for the @@ -579,7 +613,7 @@ class Screen : public concurrency::OSThread // Handle observer events int handleStatusUpdate(const meshtastic::Status *arg); - int handleTextMessage(const meshtastic_MeshPacket *arg); + int handleTextMessage(const meshtastic_MeshPacket *packet); int handleUIFrameEvent(const UIFrameEvent *arg); int handleInputEvent(const InputEvent *arg); int handleAdminMessage(AdminModule_ObserverData *arg); @@ -590,9 +624,6 @@ class Screen : public concurrency::OSThread /// Draws our SSL cert screen during boot (called from WebServer) void setSSLFrames(); - // Dismiss the currently focussed frame, if possible (e.g. text message, waypoint) - void hideCurrentFrame(); - // Menu-driven Show / Hide Toggle void toggleFrameVisibility(const std::string &frameName); bool isFrameHidden(const std::string &frameName) const; @@ -640,8 +671,6 @@ class Screen : public concurrency::OSThread // Implementations of various commands, called from doTask(). void handleSetOn(bool on, FrameCallback einkScreensaver = NULL); void handleOnPress(); - void handleShowNextFrame(); - void handleShowPrevFrame(); void handleStartFirmwareUpdateScreen(); // Info collected by setFrames method. @@ -661,7 +690,8 @@ class Screen : public concurrency::OSThread uint8_t gps = 255; uint8_t home = 255; uint8_t textMessage = 255; - uint8_t nodelist = 255; + uint8_t nodelist_nodes = 255; + uint8_t nodelist_location = 255; uint8_t nodelist_lastheard = 255; uint8_t nodelist_hopsignal = 255; uint8_t nodelist_distance = 255; @@ -684,7 +714,8 @@ class Screen : public concurrency::OSThread bool home = false; bool clock = false; #ifndef USE_EINK - bool nodelist = false; + bool nodelist_nodes = false; + bool nodelist_location = false; #endif #ifdef USE_EINK bool nodelist_lastheard = false; @@ -692,7 +723,9 @@ class Screen : public concurrency::OSThread bool nodelist_distance = false; #endif #if HAS_GPS +#ifdef USE_EINK bool nodelist_bearings = false; +#endif bool gps = false; #endif bool lora = false; @@ -718,6 +751,8 @@ class Screen : public concurrency::OSThread // Whether we are showing the regular screen (as opposed to booth screen or // Bluetooth PIN screen) bool showingNormalScreen = false; + /// Track USB power state to only wake screen on actual power state changes + bool lastPowerUSBState = false; // Implementation to Adjust Brightness uint8_t brightness = BRIGHTNESS_DEFAULT; // H = 254, MH = 192, ML = 130 L = 103 diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index d54fc9958..ed2e200bb 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -16,10 +16,17 @@ #include "graphics/fonts/OLEDDisplayFontsCS.h" #endif +#ifdef OLED_GR +#include "graphics/fonts/OLEDDisplayFontsGR.h" +#endif + #if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) #include "graphics/fonts/EinkDisplayFonts.h" #endif +#ifdef OLED_GR +#define FONT_SMALL_LOCAL ArialMT_Plain_10_GR // Height: 13 +#else #ifdef OLED_PL #define FONT_SMALL_LOCAL ArialMT_Plain_10_PL #else @@ -37,6 +44,10 @@ #endif #endif #endif +#endif +#ifdef OLED_GR +#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_GR // Height: 19 +#else #ifdef OLED_PL #define FONT_MEDIUM_LOCAL ArialMT_Plain_16_PL // Height: 19 #else @@ -54,6 +65,10 @@ #endif #endif #endif +#endif +#ifdef OLED_GR +#define FONT_LARGE_LOCAL ArialMT_Plain_24_GR // Height: 28 +#else #ifdef OLED_PL #define FONT_LARGE_LOCAL ArialMT_Plain_24_PL // Height: 28 #else @@ -71,6 +86,7 @@ #endif #endif #endif +#endif #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 892285dcb..8f06fcf9f 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -8,6 +8,7 @@ #include "graphics/draw/UIRenderer.h" #include "main.h" #include "meshtastic/config.pb.h" +#include "modules/ExternalNotificationModule.h" #include "power.h" #include #include @@ -15,27 +16,48 @@ namespace graphics { -void determineResolution(int16_t screenheight, int16_t screenwidth) +ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth) { #ifdef FORCE_LOW_RES - isHighResolution = false; - return; -#endif - - if (screenwidth > 128) { - isHighResolution = true; + return ScreenResolution::Low; +#else + // Unit C6L and other ultra low res screens + if (screenwidth <= 64 || screenheight <= 48) { + return ScreenResolution::UltraLow; } + // Standard OLED screens if (screenwidth > 128 && screenheight <= 64) { - isHighResolution = false; + return ScreenResolution::Low; } + + // High Resolutions screens like T114, TDeck, TLora Pager, etc + if (screenwidth > 128) { + return ScreenResolution::High; + } + + // Default to low resolution + return ScreenResolution::Low; +#endif +} + +void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second) +{ + hour = 0; + minute = 0; + second = 0; + if (rtc_sec == 0) + return; + uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; + hour = hms / SEC_PER_HOUR; + minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + second = hms % SEC_PER_MIN; } // === Shared External State === bool hasUnreadMessage = false; -bool isMuted = false; -bool isHighResolution = false; +ScreenResolution currentResolution = ScreenResolution::Low; // === Internal State === bool isBoltVisibleShared = true; @@ -91,7 +113,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti display->setColor(BLACK); display->fillRect(0, 0, screenW, highlightHeight + 2); display->setColor(WHITE); - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { display->drawLine(0, 20, screenW, 20); } else { display->drawLine(0, 14, screenW, 14); @@ -129,7 +151,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } #endif - bool useHorizontalBattery = (isHighResolution && screenW >= screenH); + bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH); const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; int batteryX = 1; @@ -139,7 +161,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging batteryX += 1; batteryY += 2; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution); batteryX += 20; // Icon + 1 pixel } else { @@ -200,8 +222,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti 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; + int hour, minute, second; + graphics::decomposeTime(rtc_sec, hour, minute, second); snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); // === Build Date String === @@ -209,7 +231,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); char dateLine[40]; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr); } else { if (hasUnreadMessage) { @@ -284,8 +306,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } - } else if (isMuted) { - if (isHighResolution) { + } else if (externalNotificationModule->getMute()) { + if (currentResolution == ScreenResolution::High) { int iconX = iconRightEdge - mute_symbol_big_width; int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; @@ -303,7 +325,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int iconX = iconRightEdge - mute_symbol_width; int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - if (isInverted) { + if (isInverted && !force_no_invert) { display->setColor(WHITE); display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); display->setColor(BLACK); @@ -361,8 +383,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } - } else if (isMuted) { - if (isHighResolution) { + } else if (externalNotificationModule->getMute()) { + if (currentResolution == ScreenResolution::High) { 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); @@ -381,7 +403,7 @@ const int *getTextPositions(OLEDDisplay *display) { static int textPositions[7]; // Static array that persists beyond function scope - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { textPositions[0] = textZeroLine; textPositions[1] = textFirstLine_medium; textPositions[2] = textSecondLine_medium; @@ -414,8 +436,12 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y) } if (drawConnectionState) { - if (isHighResolution) { - const int scale = 2; + const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1; + display->setColor(BLACK); + display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale), + (connection_icon_height * scale) + (2 * scale)); + display->setColor(WHITE); + if (currentResolution == ScreenResolution::High) { const int bytesPerRow = (connection_icon_width + 7) / 8; int iconX = 0; int iconY = SCREEN_HEIGHT - (connection_icon_height * 2); @@ -444,18 +470,49 @@ bool isAllowedPunctuation(char c) return allowed.find(c) != std::string::npos; } +static void replaceAll(std::string &s, const std::string &from, const std::string &to) +{ + if (from.empty()) + return; + size_t pos = 0; + while ((pos = s.find(from, pos)) != std::string::npos) { + s.replace(pos, from.size(), to); + pos += to.size(); + } +} + std::string sanitizeString(const std::string &input) { std::string output; bool inReplacement = false; - for (char c : input) { - if (std::isalnum(static_cast(c)) || isAllowedPunctuation(c)) { + // Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first. + std::string s = input; + + // Curly single quotes: ‘ ’ + replaceAll(s, "\xE2\x80\x98", "'"); // U+2018 + replaceAll(s, "\xE2\x80\x99", "'"); // U+2019 + + // Curly double quotes: “ ” + replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C + replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D + + // En dash / Em dash: – — + replaceAll(s, "\xE2\x80\x93", "-"); // U+2013 + replaceAll(s, "\xE2\x80\x94", "-"); // U+2014 + + // Non-breaking space + replaceAll(s, "\xC2\xA0", " "); // U+00A0 + + // Now do your original sanitize pass over the normalized string. + for (unsigned char uc : s) { + char c = static_cast(uc); + if (std::isalnum(uc) || isAllowedPunctuation(c)) { output += c; inReplacement = false; } else { if (!inReplacement) { - output += 0xbf; // ISO-8859-1 for inverted question mark + output += static_cast(0xBF); // ISO-8859-1 for inverted question mark inReplacement = true; } } diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index b51dfea36..a8ecdfada 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -41,9 +41,11 @@ namespace graphics // Shared state (declare inside namespace) extern bool hasUnreadMessage; -extern bool isMuted; -extern bool isHighResolution; -void determineResolution(int16_t screenheight, int16_t screenwidth); +enum class ScreenResolution : uint8_t { UltraLow = 0, Low = 1, High = 2 }; +extern ScreenResolution currentResolution; +ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth); + +void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second); // 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); diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp index a332aad9a..a24f5b15c 100644 --- a/src/graphics/VirtualKeyboard.cpp +++ b/src/graphics/VirtualKeyboard.cpp @@ -354,8 +354,6 @@ void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16 if (screenHeight <= 64) { textY = boxY + (boxHeight - inputLineH) / 2; } else { - const int innerLeft = boxX + 1; - const int innerRight = boxX + boxWidth - 2; const int innerTop = boxY + 1; const int innerBottom = boxY + boxHeight - 2; diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index cc6a70957..66bbe1bfe 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -1,15 +1,10 @@ #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/draw/UIRenderer.h" -#include "graphics/emotes.h" #include "graphics/images.h" #include "main.h" @@ -23,6 +18,31 @@ namespace graphics namespace ClockRenderer { +// Segment bitmaps for numerals 0-9 stored in flash to save RAM. +// Each row is a digit, each column is a segment state (1 = on, 0 = off). +// Segment layout reference: +// +// ___1___ +// 6 | | 2 +// |_7___| +// 5 | | 3 +// |___4_| +// +// Segment order: [1, 2, 3, 4, 5, 6, 7] +// +static const uint8_t PROGMEM digitSegments[10][7] = { + {1, 1, 1, 1, 1, 1, 0}, // 0 + {0, 1, 1, 0, 0, 0, 0}, // 1 + {1, 1, 0, 1, 1, 0, 1}, // 2 + {1, 1, 1, 1, 0, 0, 1}, // 3 + {0, 1, 1, 0, 0, 1, 1}, // 4 + {1, 0, 1, 1, 0, 1, 1}, // 5 + {1, 0, 1, 1, 1, 1, 1}, // 6 + {1, 1, 1, 0, 0, 1, 0}, // 7 + {1, 1, 1, 1, 1, 1, 1}, // 8 + {1, 1, 1, 1, 0, 1, 1} // 9 +}; + void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) { uint16_t segmentWidth = SEGMENT_WIDTH * scale; @@ -30,7 +50,7 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8; - uint16_t topAndBottomX = x + (4 * scale); + uint16_t topAndBottomX = x + static_cast(4 * scale); uint16_t quarterCellHeight = cellHeight / 4; @@ -43,34 +63,16 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) 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) |⋰ - // ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ + // Read 7-segment pattern for the digit from flash + uint8_t seg[7]; + for (uint8_t i = 0; i < 7; i++) { + seg[i] = pgm_read_byte(&digitSegments[number][i]); + } uint16_t segmentWidth = SEGMENT_WIDTH * scale; uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - // segment x and y coordinates + // Precompute segment positions uint16_t segmentOneX = x + segmentHeight + 2; uint16_t segmentOneY = y; @@ -92,33 +94,21 @@ void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t n 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); - } + // Draw only the active segments + if (seg[0]) + drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); + if (seg[1]) + drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); + if (seg[2]) + drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); + if (seg[3]) + drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); + if (seg[4]) + drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); + if (seg[5]) + drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); + if (seg[6]) + drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight); } void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height) @@ -147,42 +137,6 @@ void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int heig 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(); @@ -192,7 +146,6 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 const char *titleStr = ""; // === Header === graphics::drawCommonHeader(display, x, y, titleStr, true, true); - int line = 0; uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone char timeString[16]; @@ -237,7 +190,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 float target_width = display->getWidth() * screenwidth_target_ratio; float target_height = display->getHeight() - - (isHighResolution + ((currentResolution == ScreenResolution::High) ? 46 : 33); // Be careful adjusting this number, we have to account for header and the text under the time @@ -268,10 +221,9 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 scaleInitialized = true; } - size_t len = strlen(timeString); - // calculate hours:minutes string width - uint16_t timeStringWidth = len * 5; // base spacing between characters + size_t len = strlen(timeString); + uint16_t timeStringWidth = len * 5; for (size_t i = 0; i < len; i++) { char character = timeString[i]; @@ -310,9 +262,16 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 // draw seconds string + AM/PM display->setFont(FONT_SMALL); - int xOffset = (isHighResolution) ? 0 : -1; + int xOffset = -1; + if (currentResolution == ScreenResolution::High) { + xOffset = 0; + } if (hour >= 10) { - xOffset += (isHighResolution) ? 32 : 18; + if (currentResolution == ScreenResolution::High) { + xOffset += 32; + } else { + xOffset += 18; + } } if (config.display.use_12h_clock) { @@ -320,7 +279,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 } #ifndef USE_EINK - xOffset = (isHighResolution) ? 18 : 10; + xOffset = (currentResolution == ScreenResolution::High) ? 18 : 10; if (scale >= 2.0f) { xOffset -= (int)(4.5f * scale); } @@ -339,19 +298,13 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 const char *titleStr = ""; // === Header === graphics::drawCommonHeader(display, x, y, titleStr, true, true); - int line = 0; // clock face center coordinates int16_t centerX = display->getWidth() / 2; int16_t centerY = display->getHeight() / 2; // clock face radius - int16_t radius = 0; - if (display->getHeight() < display->getWidth()) { - radius = (display->getHeight() / 2) * 0.9; - } else { - radius = (display->getWidth() / 2) * 0.9; - } + int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9; #ifdef T_WATCH_S3 radius = (display->getWidth() / 2) * 0.8; #endif @@ -366,17 +319,8 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // 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 + 4; - if (isHighResolution) { - secondsTickMarkInnerNoonY = (double)noonY + 8; - } - - // hours tick mark inner y coordinate; (third nested circle) - double hoursTickMarkInnerNoonY = (double)noonY + 6; - if (isHighResolution) { - hoursTickMarkInnerNoonY = (double)noonY + 16; - } + double secondsTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 8 : 4); + double hoursTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 16 : 6); // minute hand y coordinate int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; @@ -386,7 +330,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // hour hand radius and y coordinate int16_t hourHandRadius = radius * 0.35; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { hourHandRadius = radius * 0.55; } int16_t hourHandNoonY = centerY - hourHandRadius; @@ -396,19 +340,13 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 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, minute, second; + decomposeTime(rtc_sec, hour, minute, second); - // 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 - - bool isPM = hour >= 12; if (config.display.use_12h_clock) { - isPM = hour >= 12; + bool isPM = hour >= 12; display->setFont(FONT_SMALL); - int yOffset = isHighResolution ? 1 : 0; + int yOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; #ifdef USE_EINK yOffset += 3; #endif @@ -499,12 +437,13 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); #else #ifdef USE_EINK - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { // draw hour number display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); } #else - if (isHighResolution && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) { + if (currentResolution == ScreenResolution::High && + (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) { // draw hour number display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); } @@ -516,7 +455,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { // draw minute tick mark display->drawLine(startX, startY, endX, endY); } diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp index 629949ffd..42600ce96 100644 --- a/src/graphics/draw/CompassRenderer.cpp +++ b/src/graphics/draw/CompassRenderer.cpp @@ -48,7 +48,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, // This could draw a "N" indicator or north arrow // For now, we'll draw a simple north indicator // const float radius = 17.0f; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { radius += 4; } Point north(0, -radius); @@ -59,7 +59,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); display->setColor(BLACK); - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { 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); diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 1b3a148d6..2dca38d66 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -282,13 +282,13 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds); // Line 1 (Still) -#if !defined(M5STACK_UNITC6L) - 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()); + if (currentResolution != graphics::ScreenResolution::UltraLow) { + 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); -#endif + display->setColor(WHITE); + } // Setup string to assemble analogClock string std::string analogClock = ""; @@ -301,9 +301,8 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t 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 + int hour, min, sec; + graphics::decomposeTime(rtc_sec, hour, min, sec); char timebuf[12]; @@ -379,7 +378,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int line = 1; // === Set Title - const char *titleStr = (isHighResolution) ? "LoRa Info" : "LoRa"; + const char *titleStr = (currentResolution == ScreenResolution::High) ? "LoRa Info" : "LoRa"; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); @@ -391,11 +390,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, char shortnameble[35]; getMacAddr(dmac); snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); -#if defined(M5STACK_UNITC6L) - snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId); -#else - snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); -#endif + if (currentResolution == ScreenResolution::UltraLow) { + snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId); + } else { + 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); @@ -414,11 +413,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; if (region != nullptr) { -#if defined(M5STACK_UNITC6L) - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region); -#else - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); -#endif + if (currentResolution == ScreenResolution::UltraLow) { + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region); + } else { + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); + } } textWidth = display->getStringWidth(regionradiopreset); nameX = (SCREEN_WIDTH - textWidth) / 2; @@ -430,17 +429,17 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, float freq = RadioLibInterface::instance->getFreq(); snprintf(freqStr, sizeof(freqStr), "%.3f", freq); if (config.lora.channel_num == 0) { -#if defined(M5STACK_UNITC6L) - snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr); -#else - snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); -#endif + if (currentResolution == ScreenResolution::UltraLow) { + snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr); + } else { + snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); + } } else { -#if defined(M5STACK_UNITC6L) - snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num); -#else - snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num); -#endif + if (currentResolution == ScreenResolution::UltraLow) { + snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num); + } else { + snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz (%d)", freqStr, config.lora.channel_num); + } } size_t len = strlen(frequencyslot); if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { @@ -456,12 +455,13 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, char chUtilPercentage[10]; snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); - int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; + int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 + : display->getStringWidth(chUtil) + 5; int chUtil_y = getTextPositions(display)[line] + 3; - int chutil_bar_width = (isHighResolution) ? 100 : 50; - int chutil_bar_height = (isHighResolution) ? 12 : 7; - int extraoffset = (isHighResolution) ? 6 : 3; + int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; + int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; + int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; int chutil_percent = airTime->channelUtilizationPercent(); int centerofscreen = SCREEN_WIDTH / 2; @@ -530,15 +530,18 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x int line = 1; const int barHeight = 6; const int labelX = x; - int barsOffset = (isHighResolution) ? 24 : 0; + int barsOffset = (currentResolution == ScreenResolution::High) ? 24 : 0; #ifdef USE_EINK +#ifndef T_DECK_PRO barsOffset -= 12; #endif -#if defined(M5STACK_UNITC6L) - const int barX = x + 45 + barsOffset; -#else - const int barX = x + 40 + barsOffset; #endif + int barX = x + barsOffset; + if (currentResolution == ScreenResolution::UltraLow) { + barX += 45; + } else { + barX += 40; + } auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { if (total == 0) return; @@ -546,7 +549,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x int percent = (used * 100) / total; char combinedStr[24]; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024, total / 1024); } else { @@ -574,7 +577,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x #endif // Value string display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr); + display->drawString(SCREEN_WIDTH, getTextPositions(display)[line], combinedStr); }; // === Memory values === @@ -626,25 +629,33 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x line += 1; char appversionstr[35]; - snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", optstr(APP_VERSION)); char appversionstr_formatted[40]; - char *lastDot = strrchr(appversionstr, '.'); -#if defined(M5STACK_UNITC6L) - if (lastDot != nullptr) { - *lastDot = '\0'; // truncate string + + const char *ver = optstr(APP_VERSION); + char verbuf[32]; + strncpy(verbuf, ver, sizeof(verbuf) - 1); + verbuf[sizeof(verbuf) - 1] = '\0'; + + char *lastDot = strrchr(verbuf, '.'); + + if (currentResolution == ScreenResolution::UltraLow) { + if (lastDot != nullptr) { + *lastDot = '\0'; + } + snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf); + } else { + if (lastDot) { + size_t prefixLen = (size_t)(lastDot - verbuf); + snprintf(appversionstr_formatted, sizeof(appversionstr_formatted), "Ver: %.*s", (int)prefixLen, verbuf); + strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); + strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); + strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); + strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1); + appversionstr[sizeof(appversionstr) - 1] = '\0'; + } else { + snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf); + } } -#else - if (lastDot) { - size_t prefixLen = lastDot - appversionstr; - strncpy(appversionstr_formatted, appversionstr, prefixLen); - appversionstr_formatted[prefixLen] = '\0'; - strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); - strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); - strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); - strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1); - appversionstr[sizeof(appversionstr) - 1] = '\0'; - } -#endif int textWidth = display->getStringWidth(appversionstr); int nameX = (SCREEN_WIDTH - textWidth) / 2; @@ -663,7 +674,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x const char *clientWord = nullptr; // Determine if narrow or wide screen - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { clientWord = "Client"; } else { clientWord = "App"; @@ -704,11 +715,23 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1 int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3); int iconY = (SCREEN_HEIGHT - chirpy_height) / 2; int textX_offset = 10; - if (isHighResolution) { - iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3); - iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2; + if (currentResolution == ScreenResolution::High) { textX_offset = textX_offset * 4; - display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez); + const int scale = 2; + const int bytesPerRow = (chirpy_width + 7) / 8; + + for (int yy = 0; yy < chirpy_height; ++yy) { + iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3); + iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2; + const uint8_t *rowPtr = chirpy + yy * bytesPerRow; + for (int xx = 0; xx < chirpy_width; ++xx) { + const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3)); + const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first + if (byteVal & bitMask) { + display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale); + } + } + } } else { display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy); } diff --git a/src/graphics/draw/DrawRenderers.h b/src/graphics/draw/DrawRenderers.h index 6f1929ebd..c55e66ede 100644 --- a/src/graphics/draw/DrawRenderers.h +++ b/src/graphics/draw/DrawRenderers.h @@ -11,7 +11,6 @@ #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 @@ -30,8 +29,6 @@ using namespace ClockRenderer; using namespace CompassRenderer; using namespace DebugRenderer; using namespace NodeListRenderer; -using namespace ScreenRenderer; -using namespace UIRenderer; } // namespace DrawRenderers diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index f782dabb6..6d29e9f7f 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -1,14 +1,17 @@ #include "configuration.h" #if HAS_SCREEN #include "ClockRenderer.h" +#include "Default.h" #include "GPS.h" #include "MenuHandler.h" #include "MeshRadio.h" #include "MeshService.h" +#include "MessageStore.h" #include "NodeDB.h" #include "buzz.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/draw/MessageRenderer.h" #include "graphics/draw/UIRenderer.h" #include "input/RotaryEncoderInterruptImpl1.h" #include "input/UpDownInterruptImpl1.h" @@ -17,27 +20,57 @@ #include "mesh/MeshTypes.h" #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" +#include "modules/ExternalNotificationModule.h" #include "modules/KeyVerificationModule.h" - #include "modules/TraceRouteModule.h" +#include +#include #include +#include extern uint16_t TFT_MESH; namespace graphics { + +namespace +{ + +// Caller must ensure the provided options array outlives the banner callback. +template +BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOption (&options)[N], + std::array &labels, Callback &&onSelection) +{ + for (size_t i = 0; i < N; ++i) { + labels[i] = options[i].label; + } + + const MenuOption *optionsPtr = options; + auto callback = std::function &, int)>(std::forward(onSelection)); + + BannerOverlayOptions bannerOptions; + bannerOptions.message = message; + bannerOptions.optionsArrayPtr = labels.data(); + bannerOptions.optionsCount = static_cast(N); + bannerOptions.bannerCallback = [optionsPtr, callback](int selected) -> void { callback(optionsPtr[selected], selected); }; + return bannerOptions; +} + +} // namespace + menuHandler::screenMenus menuHandler::menuQueue = menu_none; +uint32_t menuHandler::pickedNodeNum = 0; bool test_enabled = false; uint8_t test_count = 0; void menuHandler::loraMenu() { - static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "LoRa Region"}; - enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, lora_picker = 3 }; + static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"}; + enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, frequency_slot = 3, lora_picker = 4 }; BannerOverlayOptions bannerOptions; bannerOptions.message = "LoRa Actions"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 4; + bannerOptions.optionsCount = 5; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { // No action @@ -45,6 +78,8 @@ void menuHandler::loraMenu() menuHandler::menuQueue = menuHandler::device_role_picker; } else if (selected == radio_preset_picker) { menuHandler::menuQueue = menuHandler::radio_preset_picker; + } else if (selected == frequency_slot) { + menuHandler::menuQueue = menuHandler::frequency_slot; } else if (selected == lora_picker) { menuHandler::menuQueue = menuHandler::lora_picker; } @@ -75,51 +110,60 @@ void menuHandler::OnboardMessage() void menuHandler::LoraRegionPicker(uint32_t duration) { - static const char *optionsArray[] = {"Back", - "US", - "EU_433", - "EU_868", - "CN", - "JP", - "ANZ", - "KR", - "TW", - "RU", - "IN", - "NZ_865", - "TH", - "LORA_24", - "UA_433", - "UA_868", - "MY_433", - "MY_" - "919", - "SG_" - "923", - "PH_433", - "PH_868", - "PH_915", - "ANZ_433", - "KZ_433", - "KZ_863", - "NP_865", - "BR_902"}; - BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "LoRa Region"; -#else - bannerOptions.message = "Set the LoRa region"; -#endif - bannerOptions.durationMs = duration; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 27; - bannerOptions.InitialSelected = 0; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) { - config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected); + static const LoraRegionOption regionOptions[] = { + {"Back", OptionsAction::Back}, + {"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US}, + {"EU_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_433}, + {"EU_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_868}, + {"CN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_CN}, + {"JP", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_JP}, + {"ANZ", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ}, + {"KR", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KR}, + {"TW", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TW}, + {"RU", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_RU}, + {"IN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_IN}, + {"NZ_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NZ_865}, + {"TH", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TH}, + {"LORA_24", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_LORA_24}, + {"UA_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_433}, + {"UA_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_868}, + {"MY_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_433}, + {"MY_919", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_919}, + {"SG_923", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_SG_923}, + {"PH_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_433}, + {"PH_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_868}, + {"PH_915", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_915}, + {"ANZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ_433}, + {"KZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_433}, + {"KZ_863", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_863}, + {"NP_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NP_865}, + {"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902}, + }; + + constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]); + static std::array regionLabels{}; + + const char *bannerMessage = "Set the LoRa region"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerMessage = "LoRa Region"; + } + + auto bannerOptions = + createStaticBannerOptions(bannerMessage, regionOptions, regionLabels, [](const LoraRegionOption &option, int) -> void { + if (!option.hasValue) { + return; + } + + auto selectedRegion = option.value; + if (config.lora.region == selectedRegion) { + return; + } + + config.lora.region = selectedRegion; auto changes = SEGMENT_CONFIG; - // This is needed as we wait til picking the LoRa region to generate keys for the first time. + // FIXME: This should be a method consolidated with the same logic in the admin message as well + // This is needed as we wait til picking the LoRa region to generate keys for the first time. #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) if (!owner.is_licensed) { bool keygenSuccess = false; @@ -156,8 +200,19 @@ void menuHandler::LoraRegionPicker(uint32_t duration) service->reloadConfig(changes); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }); + + bannerOptions.durationMs = duration; + + int initialSelection = 0; + for (size_t i = 0; i < regionCount; ++i) { + if (regionOptions[i].hasValue && regionOptions[i].value == config.lora.region) { + initialSelection = static_cast(i); + break; } - }; + } + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } @@ -195,50 +250,105 @@ void menuHandler::DeviceRolePicker() screen->showOverlayBanner(bannerOptions); } -void menuHandler::RadioPresetPicker() +void menuHandler::FrequencySlotPicker() { - static const char *optionsArray[] = {"Back", "LongSlow", "LongModerate", "LongFast", "MediumSlow", - "MediumFast", "ShortSlow", "ShortFast", "ShortTurbo"}; - enum optionsNumbers { - Back = 0, - radiopreset_LongSlow = 1, - radiopreset_LongModerate = 2, - radiopreset_LongFast = 3, - radiopreset_MediumSlow = 4, - radiopreset_MediumFast = 5, - radiopreset_ShortSlow = 6, - radiopreset_ShortFast = 7, - radiopreset_ShortTurbo = 8 - }; + + enum ReplyOptions : int { Back = -1 }; + constexpr int MAX_CHANNEL_OPTIONS = 202; + static const char *optionsArray[MAX_CHANNEL_OPTIONS]; + static int optionsEnumArray[MAX_CHANNEL_OPTIONS]; + static char channelText[MAX_CHANNEL_OPTIONS - 1][12]; + int options = 0; + optionsArray[options] = "Back"; + optionsEnumArray[options++] = Back; + optionsArray[options] = "Slot 0 (Auto)"; + optionsEnumArray[options++] = 0; + + // Calculate number of channels (copied from RadioInterface::applyModemConfig()) + meshtastic_Config_LoRaConfig &loraConfig = config.lora; + double bw = loraConfig.use_preset ? modemPresetToBwKHz(loraConfig.modem_preset, myRegion->wideLora) + : bwCodeToKHz(loraConfig.bandwidth); + + uint32_t numChannels = 0; + if (myRegion) { + numChannels = (uint32_t)floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000.0))); + } else { + LOG_WARN("Region not set, cannot calculate number of channels"); + return; + } + + if (numChannels > (uint32_t)(MAX_CHANNEL_OPTIONS - 2)) + numChannels = (uint32_t)(MAX_CHANNEL_OPTIONS - 2); + + for (uint32_t ch = 1; ch <= numChannels; ch++) { + snprintf(channelText[ch - 1], sizeof(channelText[ch - 1]), "Slot %lu", (unsigned long)ch); + optionsArray[options] = channelText[ch - 1]; + optionsEnumArray[options++] = (int)ch; + } + BannerOverlayOptions bannerOptions; - bannerOptions.message = "Radio Preset"; + bannerOptions.message = "Frequency Slot"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 9; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + + // Start highlight on current channel if possible, otherwise on "1" + int initial = (int)config.lora.channel_num + 1; + if (initial < 2 || initial > (int)numChannels + 1) + initial = 1; + bannerOptions.InitialSelected = initial; + bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { menuHandler::menuQueue = menuHandler::lora_Menu; screen->runNow(); return; - } else if (selected == radiopreset_LongSlow) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW; - } else if (selected == radiopreset_LongModerate) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE; - } else if (selected == radiopreset_LongFast) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; - } else if (selected == radiopreset_MediumSlow) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW; - } else if (selected == radiopreset_MediumFast) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; - } else if (selected == radiopreset_ShortSlow) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW; - } else if (selected == radiopreset_ShortFast) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST; - } else if (selected == radiopreset_ShortTurbo) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; } + + config.lora.channel_num = selected; service->reloadConfig(SEGMENT_CONFIG); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); }; + + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::RadioPresetPicker() +{ + static const RadioPresetOption presetOptions[] = { + {"Back", OptionsAction::Back}, + {"LongTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO}, + {"LongModerate", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE}, + {"LongFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST}, + {"MediumSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW}, + {"MediumFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST}, + {"ShortSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW}, + {"ShortFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST}, + {"ShortTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO}, + }; + + constexpr size_t presetCount = sizeof(presetOptions) / sizeof(presetOptions[0]); + static std::array presetLabels{}; + + auto bannerOptions = + createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuHandler::menuQueue = menuHandler::lora_Menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + config.lora.modem_preset = option.value; + config.lora.channel_num = 0; // Reset to default channel for the preset + config.lora.override_frequency = 0; // Clear any custom frequency + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }); + screen->showOverlayBanner(bannerOptions); } @@ -282,102 +392,100 @@ void menuHandler::showConfirmationBanner(const char *message, std::function void { - if (selected == Back) { - menuHandler::menuQueue = menuHandler::clock_menu; - screen->runNow(); - } else if (selected == Digital) { - uiconfig.is_clockface_analog = false; - saveUIConfig(); - screen->setFrames(Screen::FOCUS_CLOCK); - } else { - uiconfig.is_clockface_analog = true; - saveUIConfig(); - screen->setFrames(Screen::FOCUS_CLOCK); - } + static const ClockFaceOption clockFaceOptions[] = { + {"Back", OptionsAction::Back}, + {"Digital", OptionsAction::Select, false}, + {"Analog", OptionsAction::Select, true}, }; + + constexpr size_t clockFaceCount = sizeof(clockFaceOptions) / sizeof(clockFaceOptions[0]); + static std::array clockFaceLabels{}; + + auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels, + [](const ClockFaceOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (uiconfig.is_clockface_analog == option.value) { + return; + } + + uiconfig.is_clockface_analog = option.value; + saveUIConfig(); + screen->setFrames(Screen::FOCUS_CLOCK); + }); + bannerOptions.InitialSelected = uiconfig.is_clockface_analog ? 2 : 1; screen->showOverlayBanner(bannerOptions); } void menuHandler::TZPicker() { - static const char *optionsArray[] = {"Back", - "US/Hawaii", - "US/Alaska", - "US/Pacific", - "US/Arizona", - "US/Mountain", - "US/Central", - "US/Eastern", - "BR/Brasilia", - "UTC", - "EU/Western", - "EU/" - "Central", - "EU/Eastern", - "Asia/Kolkata", - "Asia/Hong_Kong", - "AU/AWST", - "AU/ACST", - "AU/AEST", - "Pacific/NZ"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Pick Timezone"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 19; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 0) { - menuHandler::menuQueue = menuHandler::clock_menu; - screen->runNow(); - } else 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) { // Arizona - strncpy(config.device.tzdef, "MST7", sizeof(config.device.tzdef)); - } else if (selected == 5) { // Mountain - strncpy(config.device.tzdef, "MST7MDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 6) { // Central - strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 7) { // Eastern - strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 8) { // Brazil - strncpy(config.device.tzdef, "BRT3", sizeof(config.device.tzdef)); - } else if (selected == 9) { // UTC - strncpy(config.device.tzdef, "UTC0", sizeof(config.device.tzdef)); - } else if (selected == 10) { // EU/Western - strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef)); - } else if (selected == 11) { // EU/Central - strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef)); - } else if (selected == 12) { // EU/Eastern - strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef)); - } else if (selected == 13) { // Asia/Kolkata - strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef)); - } else if (selected == 14) { // China - strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef)); - } else if (selected == 15) { // AU/AWST - strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef)); - } else if (selected == 16) { // AU/ACST - strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); - } else if (selected == 17) { // AU/AEST - strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); - } else if (selected == 18) { // NZ - strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef)); - } - if (selected != 0) { + static const TimezoneOption timezoneOptions[] = { + {"Back", OptionsAction::Back}, + {"US/Hawaii", OptionsAction::Select, "HST10"}, + {"US/Alaska", OptionsAction::Select, "AKST9AKDT,M3.2.0,M11.1.0"}, + {"US/Pacific", OptionsAction::Select, "PST8PDT,M3.2.0,M11.1.0"}, + {"US/Arizona", OptionsAction::Select, "MST7"}, + {"US/Mountain", OptionsAction::Select, "MST7MDT,M3.2.0,M11.1.0"}, + {"US/Central", OptionsAction::Select, "CST6CDT,M3.2.0,M11.1.0"}, + {"US/Eastern", OptionsAction::Select, "EST5EDT,M3.2.0,M11.1.0"}, + {"BR/Brasilia", OptionsAction::Select, "BRT3"}, + {"UTC", OptionsAction::Select, "UTC0"}, + {"EU/Western", OptionsAction::Select, "GMT0BST,M3.5.0/1,M10.5.0"}, + {"EU/Central", OptionsAction::Select, "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"EU/Eastern", OptionsAction::Select, "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Asia/Kolkata", OptionsAction::Select, "IST-5:30"}, + {"Asia/Hong_Kong", OptionsAction::Select, "HKT-8"}, + {"AU/AWST", OptionsAction::Select, "AWST-8"}, + {"AU/ACST", OptionsAction::Select, "ACST-9:30ACDT,M10.1.0,M4.1.0/3"}, + {"AU/AEST", OptionsAction::Select, "AEST-10AEDT,M10.1.0,M4.1.0/3"}, + {"Pacific/NZ", OptionsAction::Select, "NZST-12NZDT,M9.5.0,M4.1.0/3"}, + }; + + constexpr size_t timezoneCount = sizeof(timezoneOptions) / sizeof(timezoneOptions[0]); + static std::array timezoneLabels{}; + + auto bannerOptions = createStaticBannerOptions( + "Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (strncmp(config.device.tzdef, option.value, sizeof(config.device.tzdef)) == 0) { + return; + } + + strncpy(config.device.tzdef, option.value, sizeof(config.device.tzdef)); + config.device.tzdef[sizeof(config.device.tzdef) - 1] = '\0'; + setenv("TZ", config.device.tzdef, 1); service->reloadConfig(SEGMENT_CONFIG); + }); + + int initialSelection = 0; + for (size_t i = 0; i < timezoneCount; ++i) { + if (timezoneOptions[i].hasValue && + strncmp(config.device.tzdef, timezoneOptions[i].value, sizeof(config.device.tzdef)) == 0) { + initialSelection = static_cast(i); + break; } - }; + } + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } @@ -407,72 +515,427 @@ void menuHandler::clockMenu() }; screen->showOverlayBanner(bannerOptions); } - void menuHandler::messageResponseMenu() { - enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3, Aloud = 4, enumEnd = 5 }; -#if defined(M5STACK_UNITC6L) - static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply Preset"}; -#else - static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply via Preset"}; -#endif - static int optionsEnumArray[enumEnd] = {Back, Dismiss, Preset}; - int options = 3; + enum optionsNumbers { Back = 0, ViewMode, DeleteMenu, ReplyMenu, MuteChannel, Aloud, enumEnd }; - if (kb_found) { - optionsArray[options] = "Reply via Freetext"; - optionsEnumArray[options++] = Freetext; + static const char *optionsArray[enumEnd]; + static int optionsEnumArray[enumEnd]; + int options = 0; + + auto mode = graphics::MessageRenderer::getThreadMode(); + int threadChannel = graphics::MessageRenderer::getThreadChannel(); + + optionsArray[options] = "Back"; + optionsEnumArray[options++] = Back; + + // New Reply submenu (replaces Preset and Freetext directly in this menu) + optionsArray[options] = "Reply"; + optionsEnumArray[options++] = ReplyMenu; + + optionsArray[options] = "View Chats"; + optionsEnumArray[options++] = ViewMode; + + // If viewing ALL chats, hide “Mute Chat” + if (mode != graphics::MessageRenderer::ThreadMode::ALL && mode != graphics::MessageRenderer::ThreadMode::DIRECT) { + const uint8_t chIndex = (threadChannel != 0) ? (uint8_t)threadChannel : channels.getPrimaryIndex(); + auto &chan = channels.getByIndex(chIndex); + + optionsArray[options] = chan.settings.module_settings.is_muted ? "Unmute Channel" : "Mute Channel"; + optionsEnumArray[options++] = MuteChannel; } + // Delete submenu + optionsArray[options] = "Delete"; + optionsEnumArray[options++] = DeleteMenu; + #ifdef HAS_I2S optionsArray[options] = "Read Aloud"; optionsEnumArray[options++] = Aloud; #endif + BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "Message"; -#else bannerOptions.message = "Message Action"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Message"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Dismiss) { - screen->hideCurrentFrame(); - } else if (selected == Preset) { - 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); + LOG_DEBUG("messageResponseMenu: selected %d", selected); + + auto mode = graphics::MessageRenderer::getThreadMode(); + int ch = graphics::MessageRenderer::getThreadChannel(); + uint32_t peer = graphics::MessageRenderer::getThreadPeer(); + + LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer); + + if (selected == ViewMode) { + menuHandler::menuQueue = menuHandler::message_viewmode_menu; + screen->runNow(); + + // Reply submenu + } else if (selected == ReplyMenu) { + menuHandler::menuQueue = menuHandler::reply_menu; + screen->runNow(); + + } else if (selected == MuteChannel) { + const uint8_t chIndex = (ch != 0) ? (uint8_t)ch : channels.getPrimaryIndex(); + auto &chan = channels.getByIndex(chIndex); + if (chan.settings.has_module_settings) { + chan.settings.module_settings.is_muted = !chan.settings.module_settings.is_muted; + nodeDB->saveToDisk(); } - } else if (selected == Freetext) { - 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); - } - } + + } else if (selected == DeleteMenu) { + menuHandler::menuQueue = menuHandler::delete_messages_menu; + screen->runNow(); + #ifdef HAS_I2S - else if (selected == Aloud) { + } else if (selected == Aloud) { const meshtastic_MeshPacket &mp = devicestate.rx_text_message; const char *msg = reinterpret_cast(mp.decoded.payload.bytes); - audioThread->readAloud(msg); - } #endif + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::replyMenu() +{ + enum replyOptions { Back = 0, ReplyPreset, ReplyFreetext, enumEnd }; + + static const char *optionsArray[enumEnd]; + static int optionsEnumArray[enumEnd]; + int options = 0; + + // Back + optionsArray[options] = "Back"; + optionsEnumArray[options++] = Back; + + // Preset reply + optionsArray[options] = "With Preset"; + optionsEnumArray[options++] = ReplyPreset; + + // Freetext reply (only when keyboard exists) + if (kb_found) { + optionsArray[options] = "With Freetext"; + optionsEnumArray[options++] = ReplyFreetext; + } + + BannerOverlayOptions bannerOptions; + + // Dynamic title based on thread mode + auto mode = graphics::MessageRenderer::getThreadMode(); + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + bannerOptions.message = "Reply to Channel"; + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + bannerOptions.message = "Reply to DM"; + } else { + // View All + bannerOptions.message = "Reply to Last Msg"; + } + + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.InitialSelected = 1; + + bannerOptions.bannerCallback = [](int selected) -> void { + auto mode = graphics::MessageRenderer::getThreadMode(); + int ch = graphics::MessageRenderer::getThreadChannel(); + uint32_t peer = graphics::MessageRenderer::getThreadPeer(); + + if (selected == Back) { + menuHandler::menuQueue = menuHandler::message_response_menu; + screen->runNow(); + return; + } + + // Preset reply + if (selected == ReplyPreset) { + + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch); + + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + cannedMessageModule->LaunchWithDestination(peer); + + } else { + // Fallback for last received message + 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); + } + } + + return; + } + + // Freetext reply + if (selected == ReplyFreetext) { + + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch); + + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + cannedMessageModule->LaunchFreetextWithDestination(peer); + + } else { + // Fallback for last received message + 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); + } + } + + return; + } + }; + screen->showOverlayBanner(bannerOptions); +} +void menuHandler::deleteMessagesMenu() +{ + enum optionsNumbers { Back = 0, DeleteOldest, DeleteThis, DeleteAll, enumEnd }; + + static const char *optionsArray[enumEnd]; + static int optionsEnumArray[enumEnd]; + int options = 0; + + auto mode = graphics::MessageRenderer::getThreadMode(); + + optionsArray[options] = "Back"; + optionsEnumArray[options++] = Back; + + optionsArray[options] = "Delete Oldest"; + optionsEnumArray[options++] = DeleteOldest; + + // If viewing ALL chats → hide “Delete This Chat” + if (mode != graphics::MessageRenderer::ThreadMode::ALL) { + optionsArray[options] = "Delete This Chat"; + optionsEnumArray[options++] = DeleteThis; + } + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "Delete All"; + } else { + optionsArray[options] = "Delete All Chats"; + } + optionsEnumArray[options++] = DeleteAll; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Delete Messages"; + + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [mode](int selected) -> void { + int ch = graphics::MessageRenderer::getThreadChannel(); + uint32_t peer = graphics::MessageRenderer::getThreadPeer(); + + if (selected == Back) { + menuHandler::menuQueue = menuHandler::message_response_menu; + screen->runNow(); + return; + } + + if (selected == DeleteAll) { + LOG_INFO("Deleting all messages"); + messageStore.clearAllMessages(); + graphics::MessageRenderer::clearThreadRegistries(); + graphics::MessageRenderer::clearMessageCache(); + return; + } + + if (selected == DeleteOldest) { + LOG_INFO("Deleting oldest message"); + + if (mode == graphics::MessageRenderer::ThreadMode::ALL) { + messageStore.deleteOldestMessage(); + } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + messageStore.deleteOldestMessageInChannel(ch); + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + messageStore.deleteOldestMessageWithPeer(peer); + } + return; + } + + // This only appears in non-ALL modes + if (selected == DeleteThis) { + LOG_INFO("Deleting all messages in this thread"); + + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + messageStore.deleteAllMessagesInChannel(ch); + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + messageStore.deleteAllMessagesWithPeer(peer); + } + return; + } + }; + + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::messageViewModeMenu() +{ + auto encodeChannelId = [](int ch) -> int { return 100 + ch; }; + auto isChannelSel = [](int id) -> bool { return id >= 100 && id < 200; }; + + static std::vector labels; + static std::vector ids; + static std::vector idToPeer; // DM lookup + + labels.clear(); + ids.clear(); + idToPeer.clear(); + + labels.push_back("Back"); + ids.push_back(-1); + labels.push_back("View All Chats"); + ids.push_back(-2); + + // Channels with messages + for (int ch = 0; ch < 8; ++ch) { + auto msgs = messageStore.getChannelMessages((uint8_t)ch); + if (!msgs.empty()) { + char buf[40]; + const char *cname = channels.getName(ch); + snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch); + labels.push_back(buf); + ids.push_back(encodeChannelId(ch)); + LOG_DEBUG("messageViewModeMenu: Added live channel %s (id=%d)", buf, encodeChannelId(ch)); + } + } + + // Registry channels + for (int ch : graphics::MessageRenderer::getSeenChannels()) { + if (ch < 0 || ch >= 8) + continue; + auto msgs = messageStore.getChannelMessages((uint8_t)ch); + if (msgs.empty()) + continue; + int enc = encodeChannelId(ch); + if (std::find(ids.begin(), ids.end(), enc) == ids.end()) { + char buf[40]; + const char *cname = channels.getName(ch); + snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch); + labels.push_back(buf); + ids.push_back(enc); + LOG_DEBUG("messageViewModeMenu: Added registry channel %s (id=%d)", buf, enc); + } + } + + // Gather unique peers + auto dms = messageStore.getDirectMessages(); + std::vector uniquePeers; + for (auto &m : dms) { + uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; + if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) + uniquePeers.push_back(peer); + } + for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) { + if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) + uniquePeers.push_back(peer); + } + std::sort(uniquePeers.begin(), uniquePeers.end()); + + // Encode peers + for (size_t i = 0; i < uniquePeers.size(); ++i) { + uint32_t peer = uniquePeers[i]; + auto node = nodeDB->getMeshNode(peer); + std::string name; + if (node && node->has_user) + name = sanitizeString(node->user.long_name).substr(0, 15); + else { + char buf[20]; + snprintf(buf, sizeof(buf), "Node %08X", peer); + name = buf; + } + labels.push_back("@" + name); + int encPeer = 1000 + (int)idToPeer.size(); + ids.push_back(encPeer); + idToPeer.push_back(peer); + LOG_DEBUG("messageViewModeMenu: Added DM %s peer=0x%08x id=%d", name.c_str(), (unsigned int)peer, encPeer); + } + + // Active ID + int activeId = -2; + auto mode = graphics::MessageRenderer::getThreadMode(); + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) + activeId = encodeChannelId(graphics::MessageRenderer::getThreadChannel()); + else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + uint32_t cur = graphics::MessageRenderer::getThreadPeer(); + for (size_t i = 0; i < idToPeer.size(); ++i) + if (idToPeer[i] == cur) { + activeId = 1000 + (int)i; + break; + } + } + + LOG_DEBUG("messageViewModeMenu: Active thread id=%d", activeId); + + // Build banner + static std::vector options; + static std::vector optionIds; + options.clear(); + optionIds.clear(); + + int initialIndex = 0; + for (size_t i = 0; i < labels.size(); i++) { + options.push_back(labels[i].c_str()); + optionIds.push_back(ids[i]); + if (ids[i] == activeId) + initialIndex = (int)i; + } + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Select Conversation"; + bannerOptions.optionsArrayPtr = options.data(); + bannerOptions.optionsEnumPtr = optionIds.data(); + bannerOptions.optionsCount = options.size(); + bannerOptions.InitialSelected = initialIndex; + + bannerOptions.bannerCallback = [=](int selected) -> void { + LOG_DEBUG("messageViewModeMenu: selected=%d", selected); + if (selected == -1) { + menuHandler::menuQueue = menuHandler::message_response_menu; + screen->runNow(); + } else if (selected == -2) { + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL); + } else if (isChannelSel(selected)) { + int ch = selected - 100; + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch); + } else if (selected >= 1000) { + int idx = selected - 1000; + if (idx >= 0 && (size_t)idx < idToPeer.size()) { + uint32_t peer = idToPeer[idx]; + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, peer); + } + } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::homeBaseMenu() { - enum optionsNumbers { Back, Backlight, Position, Preset, Freetext, Sleep, enumEnd }; + enum optionsNumbers { Back, Mute, Backlight, Position, Preset, Freetext, Sleep, enumEnd }; static const char *optionsArray[enumEnd] = {"Back"}; static int optionsEnumArray[enumEnd] = {Back}; int options = 1; + if (moduleConfig.external_notification.enabled && externalNotificationModule && + config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED) { + if (!externalNotificationModule->getMute()) { + optionsArray[options] = "Temporarily Mute"; + } else { + optionsArray[options] = "Unmute"; + } + optionsEnumArray[options++] = Mute; + } #if defined(PIN_EINK_EN) || defined(PCA_PIN_EINK_EN) optionsArray[options] = "Toggle Backlight"; optionsEnumArray[options++] = Backlight; @@ -486,28 +949,23 @@ void menuHandler::homeBaseMenu() optionsArray[options] = "Send Node Info"; } optionsEnumArray[options++] = Position; -#if defined(M5STACK_UNITC6L) - optionsArray[options] = "New Preset"; -#else - optionsArray[options] = "New Preset Msg"; -#endif - optionsEnumArray[options++] = Preset; - if (kb_found) { - optionsArray[options] = "New Freetext Msg"; - optionsEnumArray[options++] = Freetext; - } BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "Home"; -#else bannerOptions.message = "Home Action"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Home"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Backlight) { + if (selected == Mute) { + if (moduleConfig.external_notification.enabled && externalNotificationModule) { + externalNotificationModule->setMute(!externalNotificationModule->getMute()); + IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow();) + } + } else if (selected == Backlight) { + screen->setOn(false); #if defined(PIN_EINK_EN) if (uiconfig.screen_brightness == 1) { uiconfig.screen_brightness = 0; @@ -530,8 +988,12 @@ void menuHandler::homeBaseMenu() } else if (selected == Sleep) { screen->setOn(false); } else if (selected == Position) { - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SEND_PING, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); + service->refreshLocalMeshNode(); + if (service->trySendPosition(NODENUM_BROADCAST, true)) { + IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000)); + } else { + IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000)); + } } else if (selected == Preset) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); } else if (selected == Freetext) { @@ -584,24 +1046,26 @@ void menuHandler::systemBaseMenu() optionsArray[options] = "Notifications"; optionsEnumArray[options++] = Notifications; + optionsArray[options] = "Display Options"; optionsEnumArray[options++] = ScreenOptions; -#if defined(M5STACK_UNITC6L) - optionsArray[options] = "Bluetooth"; -#else - optionsArray[options] = "Bluetooth Toggle"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "Bluetooth"; + } else { + optionsArray[options] = "Bluetooth Toggle"; + } optionsEnumArray[options++] = Bluetooth; #if HAS_WIFI && !defined(ARCH_PORTDUINO) optionsArray[options] = "WiFi Toggle"; optionsEnumArray[options++] = WiFiToggle; #endif -#if defined(M5STACK_UNITC6L) - optionsArray[options] = "Power"; -#else - optionsArray[options] = "Reboot/Shutdown"; -#endif + + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "Power"; + } else { + optionsArray[options] = "Reboot/Shutdown"; + } optionsEnumArray[options++] = PowerMenu; if (test_enabled) { @@ -610,17 +1074,16 @@ void menuHandler::systemBaseMenu() } BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "System"; -#else bannerOptions.message = "System Action"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "System"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Notifications) { - menuHandler::menuQueue = menuHandler::notifications_menu; + menuHandler::menuQueue = menuHandler::buzzermodemenupicker; screen->runNow(); } else if (selected == ScreenOptions) { menuHandler::menuQueue = menuHandler::screen_options_menu; @@ -651,32 +1114,49 @@ void menuHandler::systemBaseMenu() void menuHandler::favoriteBaseMenu() { - enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd }; -#if defined(M5STACK_UNITC6L) - static const char *optionsArray[enumEnd] = {"Back", "New Preset"}; -#else - static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"}; -#endif - static int optionsEnumArray[enumEnd] = {Back, Preset}; - int options = 2; + enum optionsNumbers { Back, Preset, Freetext, GoToChat, Remove, TraceRoute, enumEnd }; + + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + + // Only show "View Conversation" if a message exists with this node + uint32_t peer = graphics::UIRenderer::currentFavoriteNodeNum; + bool hasConversation = false; + for (const auto &m : messageStore.getMessages()) { + if ((m.sender == peer || m.dest == peer)) { + hasConversation = true; + break; + } + } + if (hasConversation) { + optionsArray[options] = "Go To Chat"; + optionsEnumArray[options++] = GoToChat; + } + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "New Preset"; + } else { + optionsArray[options] = "New Preset Msg"; + } + optionsEnumArray[options++] = Preset; if (kb_found) { optionsArray[options] = "New Freetext Msg"; optionsEnumArray[options++] = Freetext; } -#if !defined(M5STACK_UNITC6L) - optionsArray[options] = "Trace Route"; - optionsEnumArray[options++] = TraceRoute; -#endif + + if (currentResolution != ScreenResolution::UltraLow) { + optionsArray[options] = "Trace Route"; + optionsEnumArray[options++] = TraceRoute; + } optionsArray[options] = "Remove Favorite"; optionsEnumArray[options++] = Remove; BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "Favorites"; -#else bannerOptions.message = "Favorites Action"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Favorites"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; @@ -685,6 +1165,17 @@ void menuHandler::favoriteBaseMenu() cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); } else if (selected == Freetext) { cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); + } + // Handle new Go To Thread action + else if (selected == GoToChat) { + // Switch thread to direct conversation with this node + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, + graphics::UIRenderer::currentFavoriteNodeNum); + + // Manually create and send a UIFrameEvent to trigger the jump + UIFrameEvent evt; + evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + screen->handleUIFrameEvent(&evt); } else if (selected == Remove) { menuHandler::menuQueue = menuHandler::remove_favorite; screen->runNow(); @@ -699,96 +1190,320 @@ void menuHandler::favoriteBaseMenu() void menuHandler::positionBaseMenu() { - enum optionsNumbers { Back, GPSToggle, GPSFormat, CompassMenu, CompassCalibrate, enumEnd }; + enum class PositionAction { + GpsToggle, + GpsFormat, + CompassMenu, + CompassCalibrate, + GPSSmartPosition, + GPSUpdateInterval, + GPSPositionBroadcast + }; - static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "GPS Format", "Compass"}; - static int optionsEnumArray[enumEnd] = {Back, GPSToggle, GPSFormat, CompassMenu}; - int options = 4; + static const PositionMenuOption baseOptions[] = { + {"Back", OptionsAction::Back}, + {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, + {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, + {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, + {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, + {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, + {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, + }; - if (accelerometerThread) { - optionsArray[options] = "Compass Calibrate"; - optionsEnumArray[options++] = CompassCalibrate; - } + static const PositionMenuOption calibrateOptions[] = { + {"Back", OptionsAction::Back}, + {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, + {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, + {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, + {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, + {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, + {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, + {"Compass Calibrate", OptionsAction::Select, static_cast(PositionAction::CompassCalibrate)}, + }; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Position Action"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.optionsCount = options; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == GPSToggle) { + constexpr size_t baseCount = sizeof(baseOptions) / sizeof(baseOptions[0]); + constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); + static std::array baseLabels{}; + static std::array calibrateLabels{}; + + auto onSelection = [](const PositionMenuOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + return; + } + + if (!option.hasValue) { + return; + } + + auto action = static_cast(option.value); + switch (action) { + case PositionAction::GpsToggle: menuQueue = gps_toggle_menu; screen->runNow(); - } else if (selected == GPSFormat) { + break; + case PositionAction::GpsFormat: menuQueue = gps_format_menu; screen->runNow(); - } else if (selected == CompassMenu) { + break; + case PositionAction::CompassMenu: menuQueue = compass_point_north_menu; screen->runNow(); - } else if (selected == CompassCalibrate) { - accelerometerThread->calibrate(30); + break; + case PositionAction::CompassCalibrate: + if (accelerometerThread) { + accelerometerThread->calibrate(30); + } + break; + case PositionAction::GPSSmartPosition: + menuQueue = gps_smart_position_menu; + screen->runNow(); + break; + case PositionAction::GPSUpdateInterval: + menuQueue = gps_update_interval_menu; + screen->runNow(); + break; + case PositionAction::GPSPositionBroadcast: + menuQueue = gps_position_broadcast_menu; + screen->runNow(); + break; } }; + + BannerOverlayOptions bannerOptions; + if (accelerometerThread) { + bannerOptions = createStaticBannerOptions("GPS Action", calibrateOptions, calibrateLabels, onSelection); + } else { + bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); + } + screen->showOverlayBanner(bannerOptions); } void menuHandler::nodeListMenu() { - enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd }; -#if defined(M5STACK_UNITC6L) - static const char *optionsArray[] = {"Back", "Add Favorite", "Reset Node"}; -#else - static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"}; -#endif + enum optionsNumbers { Back, NodePicker, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + + optionsArray[options] = "Node Actions / Settings"; + optionsEnumArray[options++] = NodePicker; + + if (currentResolution != ScreenResolution::UltraLow) { + optionsArray[options] = "Show Long/Short Name"; + optionsEnumArray[options++] = NodeNameLength; + } + optionsArray[options] = "Reset NodeDB"; + optionsEnumArray[options++] = Reset; + BannerOverlayOptions bannerOptions; bannerOptions.message = "Node Action"; bannerOptions.optionsArrayPtr = optionsArray; -#if defined(M5STACK_UNITC6L) - bannerOptions.optionsCount = 3; -#else - bannerOptions.optionsCount = 5; -#endif + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Favorite) { - menuQueue = add_favorite; - screen->runNow(); - } else if (selected == Verify) { - menuQueue = key_verification_init; + if (selected == NodePicker) { + menuQueue = NodePicker_menu; screen->runNow(); } else if (selected == Reset) { menuQueue = reset_node_db_menu; screen->runNow(); - } else if (selected == TraceRoute) { - menuQueue = trace_route_menu; + } else if (selected == NodeNameLength) { + menuHandler::menuQueue = menuHandler::node_name_length_menu; screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); } +void menuHandler::NodePicker() +{ + const char *NODE_PICKER_TITLE; + if (currentResolution == ScreenResolution::UltraLow) { + NODE_PICKER_TITLE = "Pick Node"; + } else { + NODE_PICKER_TITLE = "Pick A Node"; + } + screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void { + LOG_INFO("Nodenum: %u", nodenum); + // Store the selection so the Manage Node menu knows which node to operate on + menuHandler::pickedNodeNum = nodenum; + // Keep UI favorite context in sync (used elsewhere for some node-based actions) + graphics::UIRenderer::currentFavoriteNodeNum = nodenum; + menuQueue = Manage_Node_menu; + screen->runNow(); + }); +} + +void menuHandler::ManageNodeMenu() +{ + // If we don't have a node selected yet, go fast exit + auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum); + if (!node) { + return; + } + enum optionsNumbers { Back, Favorite, Mute, TraceRoute, KeyVerification, Ignore, enumEnd }; + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + + if (node->is_favorite) { + optionsArray[options] = "Unfavorite"; + } else { + optionsArray[options] = "Favorite"; + } + optionsEnumArray[options++] = Favorite; + + bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; + if (isMuted) { + optionsArray[options] = "Unmute Notifications"; + } else { + optionsArray[options] = "Mute Notifications"; + } + optionsEnumArray[options++] = Mute; + + optionsArray[options] = "Trace Route"; + optionsEnumArray[options++] = TraceRoute; + + optionsArray[options] = "Key Verification"; + optionsEnumArray[options++] = KeyVerification; + + if (node->is_ignored) { + optionsArray[options] = "Unignore Node"; + } else { + optionsArray[options] = "Ignore Node"; + } + optionsEnumArray[options++] = Ignore; + + BannerOverlayOptions bannerOptions; + + std::string title = ""; + if (node->has_user && node->user.long_name && node->user.long_name[0]) { + title += sanitizeString(node->user.long_name).substr(0, 15); + } else { + char buf[20]; + snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num); + title += buf; + } + bannerOptions.message = title.c_str(); + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + menuQueue = node_base_menu; + screen->runNow(); + return; + } + + if (selected == Favorite) { + auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); + if (!n) { + return; + } + if (n->is_favorite) { + LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum); + nodeDB->set_favorite(false, menuHandler::pickedNodeNum); + } else { + LOG_INFO("Adding node %08X to favorites", menuHandler::pickedNodeNum); + nodeDB->set_favorite(true, menuHandler::pickedNodeNum); + } + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + return; + } + + if (selected == Mute) { + auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); + if (!n) { + return; + } + + if (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) { + n->bitfield &= ~NODEINFO_BITFIELD_IS_MUTED_MASK; + LOG_INFO("Unmuted node %08X", menuHandler::pickedNodeNum); + } else { + n->bitfield |= NODEINFO_BITFIELD_IS_MUTED_MASK; + LOG_INFO("Muted node %08X", menuHandler::pickedNodeNum); + } + nodeDB->notifyObservers(true); + nodeDB->saveToDisk(); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + return; + } + + if (selected == TraceRoute) { + LOG_INFO("Starting traceroute to %08X", menuHandler::pickedNodeNum); + if (traceRouteModule) { + traceRouteModule->startTraceRoute(menuHandler::pickedNodeNum); + } + return; + } + + if (selected == KeyVerification) { + LOG_INFO("Initiating key verification with %08X", menuHandler::pickedNodeNum); + if (keyVerificationModule) { + keyVerificationModule->sendInitialRequest(menuHandler::pickedNodeNum); + } + return; + } + + if (selected == Ignore) { + auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); + if (!n) { + return; + } + + if (n->is_ignored) { + n->is_ignored = false; + LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum); + } else { + n->is_ignored = true; + LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum); + } + nodeDB->notifyObservers(true); + nodeDB->saveToDisk(); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + return; + } + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::nodeNameLengthMenu() { - enum OptionsNumbers { Back, Long, Short }; - static const char *optionsArray[] = {"Back", "Long", "Short"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Node Name Length"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Long) { - // Set names to long - LOG_INFO("Setting names to long"); - config.display.use_long_node_name = true; - } else if (selected == Short) { - // Set names to short - LOG_INFO("Setting names to short"); - config.display.use_long_node_name = false; - } else if (selected == Back) { - menuQueue = screen_options_menu; - screen->runNow(); - } + static const NodeNameOption nodeNameOptions[] = { + {"Back", OptionsAction::Back}, + {"Long", OptionsAction::Select, true}, + {"Short", OptionsAction::Select, false}, }; - bannerOptions.InitialSelected = config.display.use_long_node_name == true ? 1 : 2; + + constexpr size_t nodeNameCount = sizeof(nodeNameOptions) / sizeof(nodeNameOptions[0]); + static std::array nodeNameLabels{}; + + auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels, + [](const NodeNameOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = node_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (config.display.use_long_node_name == option.value) { + return; + } + + config.display.use_long_node_name = option.value; + saveUIConfig(); + LOG_INFO("Setting names to %s", option.value ? "long" : "short"); + }); + + int initialSelection = config.display.use_long_node_name ? 1 : 2; + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } @@ -812,6 +1527,9 @@ void menuHandler::resetNodeDBMenu() LOG_INFO("Initiate node-db reset but keeping favorites"); nodeDB->resetNodes(1); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } else if (selected == 0) { + menuQueue = node_base_menu; + screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); @@ -819,131 +1537,392 @@ void menuHandler::resetNodeDBMenu() void menuHandler::compassNorthMenu() { - enum optionsNumbers { Back, Dynamic, Fixed, Freeze }; - static const char *optionsArray[] = {"Back", "Dynamic", "Fixed Ring", "Freeze Heading"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "North Directions?"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 4; - bannerOptions.InitialSelected = uiconfig.compass_mode + 1; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Dynamic) { - if (uiconfig.compass_mode != meshtastic_CompassMode_DYNAMIC) { - uiconfig.compass_mode = meshtastic_CompassMode_DYNAMIC; - saveUIConfig(); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - } else if (selected == Fixed) { - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) { - uiconfig.compass_mode = meshtastic_CompassMode_FIXED_RING; - saveUIConfig(); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - } else if (selected == Freeze) { - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { - uiconfig.compass_mode = meshtastic_CompassMode_FREEZE_HEADING; - saveUIConfig(); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - } else if (selected == Back) { - menuQueue = position_base_menu; - screen->runNow(); - } + static const CompassOption compassOptions[] = { + {"Back", OptionsAction::Back}, + {"Dynamic", OptionsAction::Select, meshtastic_CompassMode_DYNAMIC}, + {"Fixed Ring", OptionsAction::Select, meshtastic_CompassMode_FIXED_RING}, + {"Freeze Heading", OptionsAction::Select, meshtastic_CompassMode_FREEZE_HEADING}, }; + + constexpr size_t compassCount = sizeof(compassOptions) / sizeof(compassOptions[0]); + static std::array compassLabels{}; + + auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels, + [](const CompassOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = position_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (uiconfig.compass_mode == option.value) { + return; + } + + uiconfig.compass_mode = option.value; + saveUIConfig(); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + }); + + int initialSelection = 0; + for (size_t i = 0; i < compassCount; ++i) { + if (compassOptions[i].hasValue && uiconfig.compass_mode == compassOptions[i].value) { + initialSelection = static_cast(i); + break; + } + } + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } #if !MESHTASTIC_EXCLUDE_GPS void menuHandler::GPSToggleMenu() { - - static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Toggle GPS"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - bannerOptions.bannerCallback = [](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); - } else { - menuQueue = position_base_menu; - screen->runNow(); - } + static const GPSToggleOption gpsToggleOptions[] = { + {"Back", OptionsAction::Back}, + {"Enabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_ENABLED}, + {"Disabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_DISABLED}, }; - bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2; + + constexpr size_t toggleCount = sizeof(gpsToggleOptions) / sizeof(gpsToggleOptions[0]); + static std::array toggleLabels{}; + + auto bannerOptions = + createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = position_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (config.position.gps_mode == option.value) { + return; + } + + config.position.gps_mode = option.value; + if (option.value == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + playGPSEnableBeep(); + gps->enable(); + } else { + playGPSDisableBeep(); + gps->disable(); + } + service->reloadConfig(SEGMENT_CONFIG); + }); + + int initialSelection = 0; + for (size_t i = 0; i < toggleCount; ++i) { + if (gpsToggleOptions[i].hasValue && config.position.gps_mode == gpsToggleOptions[i].value) { + initialSelection = static_cast(i); + break; + } + } + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } void menuHandler::GPSFormatMenu() { + static const GPSFormatOption formatOptionsHigh[] = { + {"Back", OptionsAction::Back}, + {"Decimal Degrees", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, + {"Degrees Minutes Seconds", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, + {"Universal Transverse Mercator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, + {"Military Grid Reference System", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, + {"Open Location Code", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, + {"Ordnance Survey Grid Ref", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, + {"Maidenhead Locator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, + }; - static const char *optionsArray[] = {"Back", - isHighResolution ? "Decimal Degrees" : "DEC", - isHighResolution ? "Degrees Minutes Seconds" : "DMS", - isHighResolution ? "Universal Transverse Mercator" : "UTM", - isHighResolution ? "Military Grid Reference System" : "MGRS", - isHighResolution ? "Open Location Code" : "OLC", - isHighResolution ? "Ordnance Survey Grid Ref" : "OSGR", - isHighResolution ? "Maidenhead Locator" : "MLS"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "GPS Format"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 8; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 2) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 3) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 4) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 5) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 6) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 7) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else { + static const GPSFormatOption formatOptionsLow[] = { + {"Back", OptionsAction::Back}, + {"DEC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, + {"DMS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, + {"UTM", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, + {"MGRS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, + {"OLC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, + {"OSGR", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, + {"MLS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, + }; + + constexpr size_t formatCount = sizeof(formatOptionsHigh) / sizeof(formatOptionsHigh[0]); + static std::array formatLabelsHigh{}; + static std::array formatLabelsLow{}; + + auto onSelection = [](const GPSFormatOption &option, int) -> void { + if (option.action == OptionsAction::Back) { menuQueue = position_base_menu; screen->runNow(); + return; } + + if (!option.hasValue) { + return; + } + + if (uiconfig.gps_format == option.value) { + return; + } + + uiconfig.gps_format = option.value; + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); }; - bannerOptions.InitialSelected = uiconfig.gps_format + 1; + + BannerOverlayOptions bannerOptions; + int initialSelection = 0; + + if (currentResolution == ScreenResolution::High) { + bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsHigh, formatLabelsHigh, onSelection); + for (size_t i = 0; i < formatCount; ++i) { + if (formatOptionsHigh[i].hasValue && uiconfig.gps_format == formatOptionsHigh[i].value) { + initialSelection = static_cast(i); + break; + } + } + } else { + bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsLow, formatLabelsLow, onSelection); + for (size_t i = 0; i < formatCount; ++i) { + if (formatOptionsLow[i].hasValue && uiconfig.gps_format == formatOptionsLow[i].value) { + initialSelection = static_cast(i); + break; + } + } + } + + bannerOptions.InitialSelected = initialSelection; screen->showOverlayBanner(bannerOptions); } + +void menuHandler::GPSSmartPositionMenu() +{ + static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Toggle Smart Position"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Smrt Postn"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuQueue = position_base_menu; + screen->runNow(); + } else if (selected == 1) { + config.position.position_broadcast_smart_enabled = true; + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } else if (selected == 2) { + config.position.position_broadcast_smart_enabled = false; + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + bannerOptions.InitialSelected = config.position.position_broadcast_smart_enabled ? 1 : 2; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::GPSUpdateIntervalMenu() +{ + static const char *optionsArray[] = {"Back", "8 seconds", "20 seconds", "40 seconds", "1 minute", "80 seconds", + "2 minutes", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "1 hour", + "6 hours", "12 hours", "24 hours", "At Boot Only"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Update Interval"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 16; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuQueue = position_base_menu; + screen->runNow(); + } else if (selected == 1) { + config.position.gps_update_interval = 8; + } else if (selected == 2) { + config.position.gps_update_interval = 20; + } else if (selected == 3) { + config.position.gps_update_interval = 40; + } else if (selected == 4) { + config.position.gps_update_interval = 60; + } else if (selected == 5) { + config.position.gps_update_interval = 80; + } else if (selected == 6) { + config.position.gps_update_interval = 120; + } else if (selected == 7) { + config.position.gps_update_interval = 300; + } else if (selected == 8) { + config.position.gps_update_interval = 600; + } else if (selected == 9) { + config.position.gps_update_interval = 900; + } else if (selected == 10) { + config.position.gps_update_interval = 1800; + } else if (selected == 11) { + config.position.gps_update_interval = 3600; + } else if (selected == 12) { + config.position.gps_update_interval = 21600; + } else if (selected == 13) { + config.position.gps_update_interval = 43200; + } else if (selected == 14) { + config.position.gps_update_interval = 86400; + } else if (selected == 15) { + config.position.gps_update_interval = 2147483647; // At Boot Only + } + + if (selected != 0) { + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + + if (config.position.gps_update_interval == 8) { + bannerOptions.InitialSelected = 1; + } else if (config.position.gps_update_interval == 20) { + bannerOptions.InitialSelected = 2; + } else if (config.position.gps_update_interval == 40) { + bannerOptions.InitialSelected = 3; + } else if (config.position.gps_update_interval == 60) { + bannerOptions.InitialSelected = 4; + } else if (config.position.gps_update_interval == 80) { + bannerOptions.InitialSelected = 5; + } else if (config.position.gps_update_interval == 120) { + bannerOptions.InitialSelected = 6; + } else if (config.position.gps_update_interval == 300) { + bannerOptions.InitialSelected = 7; + } else if (config.position.gps_update_interval == 600) { + bannerOptions.InitialSelected = 8; + } else if (config.position.gps_update_interval == 900) { + bannerOptions.InitialSelected = 9; + } else if (config.position.gps_update_interval == 1800) { + bannerOptions.InitialSelected = 10; + } else if (config.position.gps_update_interval == 3600) { + bannerOptions.InitialSelected = 11; + } else if (config.position.gps_update_interval == 21600) { + bannerOptions.InitialSelected = 12; + } else if (config.position.gps_update_interval == 43200) { + bannerOptions.InitialSelected = 13; + } else if (config.position.gps_update_interval == 86400) { + bannerOptions.InitialSelected = 14; + } else if (config.position.gps_update_interval == 2147483647) { // At Boot Only + bannerOptions.InitialSelected = 15; + } else { + bannerOptions.InitialSelected = 0; + } + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::GPSPositionBroadcastMenu() +{ + static const char *optionsArray[] = {"Back", "1 minute", "90 seconds", "5 minutes", "15 minutes", "1 hour", + "2 hours", "3 hours", "4 hours", "5 hours", "6 hours", "12 hours", + "18 hours", "24 hours", "36 hours", "48 hours", "72 hours"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Broadcast Interval"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 17; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuQueue = position_base_menu; + screen->runNow(); + } else if (selected == 1) { + config.position.position_broadcast_secs = 60; + } else if (selected == 2) { + config.position.position_broadcast_secs = 90; + } else if (selected == 3) { + config.position.position_broadcast_secs = 300; + } else if (selected == 4) { + config.position.position_broadcast_secs = 900; + } else if (selected == 5) { + config.position.position_broadcast_secs = 3600; + } else if (selected == 6) { + config.position.position_broadcast_secs = 7200; + } else if (selected == 7) { + config.position.position_broadcast_secs = 10800; + } else if (selected == 8) { + config.position.position_broadcast_secs = 14400; + } else if (selected == 9) { + config.position.position_broadcast_secs = 18000; + } else if (selected == 10) { + config.position.position_broadcast_secs = 21600; + } else if (selected == 11) { + config.position.position_broadcast_secs = 43200; + } else if (selected == 12) { + config.position.position_broadcast_secs = 64800; + } else if (selected == 13) { + config.position.position_broadcast_secs = 86400; + } else if (selected == 14) { + config.position.position_broadcast_secs = 129600; + } else if (selected == 15) { + config.position.position_broadcast_secs = 172800; + } else if (selected == 16) { + config.position.position_broadcast_secs = 259200; + } + + if (selected != 0) { + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + + if (config.position.position_broadcast_secs == 60) { + bannerOptions.InitialSelected = 1; + } else if (config.position.position_broadcast_secs == 90) { + bannerOptions.InitialSelected = 2; + } else if (config.position.position_broadcast_secs == 300) { + bannerOptions.InitialSelected = 3; + } else if (config.position.position_broadcast_secs == 900) { + bannerOptions.InitialSelected = 4; + } else if (config.position.position_broadcast_secs == 3600) { + bannerOptions.InitialSelected = 5; + } else if (config.position.position_broadcast_secs == 7200) { + bannerOptions.InitialSelected = 6; + } else if (config.position.position_broadcast_secs == 10800) { + bannerOptions.InitialSelected = 7; + } else if (config.position.position_broadcast_secs == 14400) { + bannerOptions.InitialSelected = 8; + } else if (config.position.position_broadcast_secs == 18000) { + bannerOptions.InitialSelected = 9; + } else if (config.position.position_broadcast_secs == 21600) { + bannerOptions.InitialSelected = 10; + } else if (config.position.position_broadcast_secs == 43200) { + bannerOptions.InitialSelected = 11; + } else if (config.position.position_broadcast_secs == 64800) { + bannerOptions.InitialSelected = 12; + } else if (config.position.position_broadcast_secs == 86400) { + bannerOptions.InitialSelected = 13; + } else if (config.position.position_broadcast_secs == 129600) { + bannerOptions.InitialSelected = 14; + } else if (config.position.position_broadcast_secs == 172800) { + bannerOptions.InitialSelected = 15; + } else if (config.position.position_broadcast_secs == 259200) { + bannerOptions.InitialSelected = 16; + } else { + bannerOptions.InitialSelected = 0; + } + screen->showOverlayBanner(bannerOptions); +} + #endif void menuHandler::BluetoothToggleMenu() { static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "Bluetooth"; -#else bannerOptions.message = "Toggle Bluetooth"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Bluetooth"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { @@ -960,9 +1939,9 @@ void menuHandler::BluetoothToggleMenu() void menuHandler::BuzzerModeMenu() { - static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only", "DMs Only"}; + static const char *optionsArray[] = {"All Enabled", "All Disabled", "Notifications", "System Only", "DMs Only"}; BannerOverlayOptions bannerOptions; - bannerOptions.message = "Buzzer Mode"; + bannerOptions.message = "Notification Sounds"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 5; bannerOptions.bannerCallback = [](int selected) -> void { @@ -1041,79 +2020,63 @@ void menuHandler::switchToMUIMenu() void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) { - static const char *optionsArray[] = {"Back", "Default", "Meshtastic Green", "Yellow", "Red", "Orange", "Purple", "Teal", - "Pink", "White"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Select Screen Color"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 10; - bannerOptions.bannerCallback = [display](int selected) -> void { + static const ScreenColorOption colorOptions[] = { + {"Back", OptionsAction::Back}, + {"Default", OptionsAction::Select, ScreenColor(0, 0, 0, true)}, + {"Meshtastic Green", OptionsAction::Select, ScreenColor(0x67, 0xEA, 0x94)}, + {"Yellow", OptionsAction::Select, ScreenColor(255, 255, 128)}, + {"Red", OptionsAction::Select, ScreenColor(255, 64, 64)}, + {"Orange", OptionsAction::Select, ScreenColor(255, 160, 20)}, + {"Purple", OptionsAction::Select, ScreenColor(204, 153, 255)}, + {"Blue", OptionsAction::Select, ScreenColor(0, 0, 255)}, + {"Teal", OptionsAction::Select, ScreenColor(16, 102, 102)}, + {"Cyan", OptionsAction::Select, ScreenColor(0, 255, 255)}, + {"Ice", OptionsAction::Select, ScreenColor(173, 216, 230)}, + {"Pink", OptionsAction::Select, ScreenColor(255, 105, 180)}, + {"White", OptionsAction::Select, ScreenColor(255, 255, 255)}, + {"Gray", OptionsAction::Select, ScreenColor(128, 128, 128)}, + }; + + constexpr size_t colorCount = sizeof(colorOptions) / sizeof(colorOptions[0]); + static std::array colorLabels{}; + + auto bannerOptions = createStaticBannerOptions( + "Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = system_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \ HAS_TFT || defined(HACKADAY_COMMUNICATOR) - uint8_t TFT_MESH_r = 0; - uint8_t TFT_MESH_g = 0; - uint8_t TFT_MESH_b = 0; - if (selected == 1) { - LOG_INFO("Setting color to system default or defined variant"); - // Given just before we set all these to zero, we will allow this to go through - } else if (selected == 2) { - LOG_INFO("Setting color to Meshtastic Green"); - TFT_MESH_r = 103; - TFT_MESH_g = 234; - TFT_MESH_b = 148; - } else if (selected == 3) { - LOG_INFO("Setting color to Yellow"); - TFT_MESH_r = 255; - TFT_MESH_g = 255; - TFT_MESH_b = 128; - } else if (selected == 4) { - LOG_INFO("Setting color to Red"); - TFT_MESH_r = 255; - TFT_MESH_g = 64; - TFT_MESH_b = 64; - } else if (selected == 5) { - LOG_INFO("Setting color to Orange"); - TFT_MESH_r = 255; - TFT_MESH_g = 160; - TFT_MESH_b = 20; - } else if (selected == 6) { - LOG_INFO("Setting color to Purple"); - TFT_MESH_r = 204; - TFT_MESH_g = 153; - TFT_MESH_b = 255; - } else if (selected == 7) { - LOG_INFO("Setting color to Teal"); - TFT_MESH_r = 64; - TFT_MESH_g = 224; - TFT_MESH_b = 208; - } else if (selected == 8) { - LOG_INFO("Setting color to Pink"); - TFT_MESH_r = 255; - TFT_MESH_g = 105; - TFT_MESH_b = 180; - } else if (selected == 9) { - LOG_INFO("Setting color to White"); - TFT_MESH_r = 255; - TFT_MESH_g = 255; - TFT_MESH_b = 255; - } else { - menuQueue = system_base_menu; - screen->runNow(); - } + const ScreenColor &color = option.value; + if (color.useVariant) { + LOG_INFO("Setting color to system default or defined variant"); + } else { + LOG_INFO("Setting color to %s", option.label); + } + + uint8_t r = color.r; + uint8_t g = color.g; + uint8_t b = color.b; - if (selected != 0) { display->setColor(BLACK); display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); display->setColor(WHITE); - if (TFT_MESH_r == 0 && TFT_MESH_g == 0 && TFT_MESH_b == 0) { + if (color.useVariant || (r == 0 && g == 0 && b == 0)) { #ifdef TFT_MESH_OVERRIDE TFT_MESH = TFT_MESH_OVERRIDE; #else - TFT_MESH = COLOR565(0x67, 0xEA, 0x94); + TFT_MESH = COLOR565(255, 255, 128); #endif } else { - TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); + TFT_MESH = COLOR565(r, g, b); } #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) @@ -1121,16 +2084,40 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) #endif screen->setFrames(graphics::Screen::FOCUS_SYSTEM); - if (TFT_MESH_r == 0 && TFT_MESH_g == 0 && TFT_MESH_b == 0) { + if (color.useVariant || (r == 0 && g == 0 && b == 0)) { uiconfig.screen_rgb_color = 0; } else { - uiconfig.screen_rgb_color = (TFT_MESH_r << 16) | (TFT_MESH_g << 8) | TFT_MESH_b; + uiconfig.screen_rgb_color = + (static_cast(r) << 16) | (static_cast(g) << 8) | static_cast(b); } LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color); saveUIConfig(); - } #endif - }; + }); + + int initialSelection = 0; + if (uiconfig.screen_rgb_color == 0) { + initialSelection = 1; + } else { + uint32_t currentColor = uiconfig.screen_rgb_color; + for (size_t i = 0; i < colorCount; ++i) { + if (!colorOptions[i].hasValue) { + continue; + } + const ScreenColor &color = colorOptions[i].value; + if (color.useVariant) { + continue; + } + uint32_t encoded = + (static_cast(color.r) << 16) | (static_cast(color.g) << 8) | static_cast(color.b); + if (encoded == currentColor) { + initialSelection = static_cast(i); + break; + } + } + } + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } @@ -1138,17 +2125,17 @@ void menuHandler::rebootMenu() { static const char *optionsArray[] = {"Back", "Confirm"}; BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "Reboot"; -#else bannerOptions.message = "Reboot Device?"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Reboot"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1) { IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); nodeDB->saveToDisk(); + messageStore.saveToFlash(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } else { menuQueue = power_menu; @@ -1162,11 +2149,10 @@ void menuHandler::shutdownMenu() { static const char *optionsArray[] = {"Back", "Confirm"}; BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "Shutdown"; -#else bannerOptions.message = "Shutdown Device?"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Shutdown"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { @@ -1181,20 +2167,6 @@ void menuHandler::shutdownMenu() screen->showOverlayBanner(bannerOptions); } -void menuHandler::addFavoriteMenu() -{ -#if defined(M5STACK_UNITC6L) - screen->showNodePicker("Node Favorite", 30000, [](uint32_t nodenum) -> void { -#else - screen->showNodePicker("Node To Favorite", 30000, [](uint32_t nodenum) -> void { - -#endif - LOG_WARN("Nodenum: %u", nodenum); - nodeDB->set_favorite(true, nodenum); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - }); -} - void menuHandler::removeFavoriteMenu() { @@ -1316,30 +2288,6 @@ void menuHandler::wifiToggleMenu() screen->showOverlayBanner(bannerOptions); } -void menuHandler::notificationsMenu() -{ - enum optionsNumbers { Back, BuzzerActions }; - static const char *optionsArray[] = {"Back", "Buzzer Actions"}; - static int optionsEnumArray[] = {Back, BuzzerActions}; - int options = 2; - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Notifications"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = options; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == BuzzerActions) { - menuHandler::menuQueue = menuHandler::buzzermodemenupicker; - screen->runNow(); - } else { - menuQueue = system_base_menu; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); -} - void menuHandler::screenOptionsMenu() { // Check if brightness is supported @@ -1353,16 +2301,11 @@ void menuHandler::screenOptionsMenu() hasSupportBrightness = false; #endif - enum optionsNumbers { Back, NodeNameLength, Brightness, ScreenColor, FrameToggles, DisplayUnits }; + enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits }; static const char *optionsArray[5] = {"Back"}; static int optionsEnumArray[5] = {Back}; int options = 1; -#if defined(T_DECK) || defined(T_LORA_PAGER) || defined(HACKADAY_COMMUNICATOR) - optionsArray[options] = "Show Long/Short Name"; - optionsEnumArray[options++] = NodeNameLength; -#endif - // Only show brightness for B&W displays if (hasSupportBrightness) { optionsArray[options] = "Brightness"; @@ -1376,7 +2319,7 @@ void menuHandler::screenOptionsMenu() optionsEnumArray[options++] = ScreenColor; #endif - optionsArray[options] = "Frame Visibility Toggle"; + optionsArray[options] = "Frame Visibility"; optionsEnumArray[options++] = FrameToggles; optionsArray[options] = "Display Units"; @@ -1394,9 +2337,6 @@ void menuHandler::screenOptionsMenu() } else if (selected == ScreenColor) { menuHandler::menuQueue = menuHandler::tftcolormenupicker; screen->runNow(); - } else if (selected == NodeNameLength) { - menuHandler::menuQueue = menuHandler::node_name_length_menu; - screen->runNow(); } else if (selected == FrameToggles) { menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); @@ -1431,11 +2371,10 @@ void menuHandler::powerMenu() #endif BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "Power"; -#else bannerOptions.message = "Reboot / Shutdown"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Power"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; @@ -1492,7 +2431,8 @@ void menuHandler::FrameToggles_menu() { enum optionsNumbers { Finish, - nodelist, + nodelist_nodes, + nodelist_location, nodelist_lastheard, nodelist_hopsignal, nodelist_distance, @@ -1501,7 +2441,8 @@ void menuHandler::FrameToggles_menu() lora, clock, show_favorites, - show_telemetry, + show_env_telemetry, + show_aq_telemetry, show_power, enumEnd }; @@ -1513,20 +2454,25 @@ void menuHandler::FrameToggles_menu() static int lastSelectedIndex = 0; #ifndef USE_EINK - optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List"; - optionsEnumArray[options++] = nodelist; -#endif -#ifdef USE_EINK + optionsArray[options] = screen->isFrameHidden("nodelist_nodes") ? "Show Node Lists" : "Hide Node Lists"; + optionsEnumArray[options++] = nodelist_nodes; +#else optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard"; optionsEnumArray[options++] = nodelist_lastheard; optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal"; optionsEnumArray[options++] = nodelist_hopsignal; +#endif + +#if HAS_GPS +#ifndef USE_EINK + optionsArray[options] = screen->isFrameHidden("nodelist_location") ? "Show Position Lists" : "Hide Position Lists"; + optionsEnumArray[options++] = nodelist_location; +#else optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance"; optionsEnumArray[options++] = nodelist_distance; -#endif -#if HAS_GPS - optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings"; + optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show NL - Bearings" : "Hide NL - Bearings"; optionsEnumArray[options++] = nodelist_bearings; +#endif optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position"; optionsEnumArray[options++] = gps; @@ -1541,8 +2487,11 @@ void menuHandler::FrameToggles_menu() optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites"; optionsEnumArray[options++] = show_favorites; - optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Telemetry" : "Show Telemetry"; - optionsEnumArray[options++] = show_telemetry; + optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Env. Telemetry" : "Show Env. Telemetry"; + optionsEnumArray[options++] = show_env_telemetry; + + optionsArray[options] = moduleConfig.telemetry.air_quality_screen_enabled ? "Hide AQ Telemetry" : "Show AQ Telemetry"; + optionsEnumArray[options++] = show_aq_telemetry; optionsArray[options] = moduleConfig.telemetry.power_screen_enabled ? "Hide Power" : "Show Power"; optionsEnumArray[options++] = show_power; @@ -1565,8 +2514,12 @@ void menuHandler::FrameToggles_menu() if (selected == Finish) { screen->setFrames(Screen::FOCUS_DEFAULT); - } else if (selected == nodelist) { - screen->toggleFrameVisibility("nodelist"); + } else if (selected == nodelist_nodes) { + screen->toggleFrameVisibility("nodelist_nodes"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_location) { + screen->toggleFrameVisibility("nodelist_location"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == nodelist_lastheard) { @@ -1601,10 +2554,14 @@ void menuHandler::FrameToggles_menu() screen->toggleFrameVisibility("show_favorites"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); - } else if (selected == show_telemetry) { + } else if (selected == show_env_telemetry) { moduleConfig.telemetry.environment_screen_enabled = !moduleConfig.telemetry.environment_screen_enabled; menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); + } else if (selected == show_aq_telemetry) { + moduleConfig.telemetry.air_quality_screen_enabled = !moduleConfig.telemetry.air_quality_screen_enabled; + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); } else if (selected == show_power) { moduleConfig.telemetry.power_screen_enabled = !moduleConfig.telemetry.power_screen_enabled; menuHandler::menuQueue = menuHandler::FrameToggles; @@ -1661,6 +2618,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case radio_preset_picker: RadioPresetPicker(); break; + case frequency_slot: + FrequencySlotPicker(); + break; case no_timeout_lora_picker: LoraRegionPicker(0); break; @@ -1682,6 +2642,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case position_base_menu: positionBaseMenu(); break; + case node_base_menu: + nodeListMenu(); + break; #if !MESHTASTIC_EXCLUDE_GPS case gps_toggle_menu: GPSToggleMenu(); @@ -1689,6 +2652,15 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case gps_format_menu: GPSFormatMenu(); break; + case gps_smart_position_menu: + GPSSmartPositionMenu(); + break; + case gps_update_interval_menu: + GPSUpdateIntervalMenu(); + break; + case gps_position_broadcast_menu: + GPSPositionBroadcastMenu(); + break; #endif case compass_point_north_menu: compassNorthMenu(); @@ -1717,8 +2689,11 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case shutdown_menu: shutdownMenu(); break; - case add_favorite: - addFavoriteMenu(); + case NodePicker_menu: + NodePicker(); + break; + case Manage_Node_menu: + ManageNodeMenu(); break; case remove_favorite: removeFavoriteMenu(); @@ -1744,9 +2719,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case bluetooth_toggle_menu: BluetoothToggleMenu(); break; - case notifications_menu: - notificationsMenu(); - break; case screen_options_menu: screenOptionsMenu(); break; @@ -1762,6 +2734,18 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case throttle_message: screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); break; + case message_response_menu: + messageResponseMenu(); + break; + case reply_menu: + replyMenu(); + break; + case delete_messages_menu: + deleteMessagesMenu(); + break; + case message_viewmode_menu: + messageViewModeMenu(); + break; } menuQueue = menu_none; } @@ -1773,4 +2757,4 @@ void menuHandler::saveUIConfig() } // namespace graphics -#endif +#endif \ No newline at end of file diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index a611b7c9d..1b964678b 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -13,14 +13,19 @@ class menuHandler lora_picker, device_role_picker, radio_preset_picker, + frequency_slot, no_timeout_lora_picker, TZ_picker, twelve_hour_picker, clock_face_picker, clock_menu, position_base_menu, + node_base_menu, gps_toggle_menu, gps_format_menu, + gps_smart_position_menu, + gps_update_interval_menu, + gps_position_broadcast_menu, compass_point_north_menu, reset_node_db_menu, buzzermodemenupicker, @@ -29,13 +34,13 @@ class menuHandler brightness_picker, reboot_menu, shutdown_menu, - add_favorite, + NodePicker_menu, + Manage_Node_menu, remove_favorite, test_menu, number_test, wifi_toggle_menu, bluetooth_toggle_menu, - notifications_menu, screen_options_menu, power_menu, system_base_menu, @@ -43,17 +48,23 @@ class menuHandler key_verification_final_prompt, trace_route_menu, throttle_message, + message_response_menu, + message_viewmode_menu, + reply_menu, + delete_messages_menu, node_name_length_menu, FrameToggles, DisplayUnits }; static screenMenus menuQueue; + static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu static void OnboardMessage(); static void LoraRegionPicker(uint32_t duration = 30000); static void loraMenu(); static void DeviceRolePicker(); static void RadioPresetPicker(); + static void FrequencySlotPicker(); static void handleMenuSwitch(OLEDDisplay *display); static void showConfirmationBanner(const char *message, std::function onConfirm); static void clockMenu(); @@ -61,6 +72,9 @@ class menuHandler static void TwelveHourPicker(); static void ClockFacePicker(); static void messageResponseMenu(); + static void messageViewModeMenu(); + static void replyMenu(); + static void deleteMessagesMenu(); static void homeBaseMenu(); static void textMessageBaseMenu(); static void systemBaseMenu(); @@ -69,6 +83,9 @@ class menuHandler static void compassNorthMenu(); static void GPSToggleMenu(); static void GPSFormatMenu(); + static void GPSSmartPositionMenu(); + static void GPSUpdateIntervalMenu(); + static void GPSPositionBroadcastMenu(); static void BuzzerModeMenu(); static void switchToMUIMenu(); static void TFTColorPickerMenu(OLEDDisplay *display); @@ -77,6 +94,8 @@ class menuHandler static void BrightnessPickerMenu(); static void rebootMenu(); static void shutdownMenu(); + static void NodePicker(); + static void ManageNodeMenu(); static void addFavoriteMenu(); static void removeFavoriteMenu(); static void traceRouteMenu(); @@ -84,7 +103,6 @@ class menuHandler static void numberTest(); static void wifiBaseMenu(); static void wifiToggleMenu(); - static void notificationsMenu(); static void screenOptionsMenu(); static void powerMenu(); static void nodeNameLengthMenu(); @@ -99,5 +117,46 @@ class menuHandler static void BluetoothToggleMenu(); }; +/* Generic Menu Options designations */ +enum class OptionsAction { Back, Select }; + +template struct MenuOption { + const char *label; + OptionsAction action; + bool hasValue; + T value; + + MenuOption(const char *labelIn, OptionsAction actionIn, T valueIn) + : label(labelIn), action(actionIn), hasValue(true), value(valueIn) + { + } + + MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {} +}; + +struct ScreenColor { + uint8_t r; + uint8_t g; + uint8_t b; + bool useVariant; + + explicit ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false) + : r(rIn), g(gIn), b(bIn), useVariant(variantIn) + { + } +}; + +using RadioPresetOption = MenuOption; +using LoraRegionOption = MenuOption; +using TimezoneOption = MenuOption; +using CompassOption = MenuOption; +using ScreenColorOption = MenuOption; +using GPSToggleOption = MenuOption; +using GPSFormatOption = MenuOption; +using NodeNameOption = MenuOption; +using PositionMenuOption = MenuOption; +using ManageNodeOption = MenuOption; +using ClockFaceOption = MenuOption; + } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index da6ec7abc..01fdbb966 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -1,51 +1,25 @@ -/* -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 "MessageStore.h" #include "NodeDB.h" -#include "configuration.h" +#include "UIRenderer.h" #include "gps/RTC.h" +#include "graphics/Screen.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/TimeFormatters.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; +extern graphics::Screen *screen; using graphics::Emote; using graphics::emotes; @@ -56,17 +30,90 @@ namespace graphics namespace MessageRenderer { -// Simple cache based on text hash -static size_t cachedKey = 0; static std::vector cachedLines; static std::vector cachedHeights; +static bool manualScrolling = false; + +// UTF-8 skip helper +static inline size_t utf8CharLen(uint8_t c) +{ + if ((c & 0xE0) == 0xC0) + return 2; + if ((c & 0xF0) == 0xE0) + return 3; + if ((c & 0xF8) == 0xF0) + return 4; + return 1; +} + +// Remove variation selectors (FE0F) and skin tone modifiers from emoji so they match your labels +static std::string normalizeEmoji(const std::string &s) +{ + std::string out; + for (size_t i = 0; i < s.size();) { + uint8_t c = static_cast(s[i]); + size_t len = utf8CharLen(c); + + if (c == 0xEF && i + 2 < s.size() && (uint8_t)s[i + 1] == 0xB8 && (uint8_t)s[i + 2] == 0x8F) { + i += 3; + continue; + } + + // Skip skin tone modifiers + if (c == 0xF0 && i + 3 < s.size() && (uint8_t)s[i + 1] == 0x9F && (uint8_t)s[i + 2] == 0x8F && + ((uint8_t)s[i + 3] >= 0xBB && (uint8_t)s[i + 3] <= 0xBF)) { + i += 4; + continue; + } + + out.append(s, i, len); + i += len; + } + return out; +} + +// Scroll state (file scope so we can reset on new message) +float scrollY = 0.0f; +uint32_t lastTime = 0; +uint32_t scrollStartDelay = 0; +uint32_t pauseStart = 0; +bool waitingToReset = false; +bool scrollStarted = false; +static bool didReset = false; +static constexpr int MESSAGE_BLOCK_GAP = 6; + +void scrollUp() +{ + manualScrolling = true; + scrollY -= 12; + if (scrollY < 0) + scrollY = 0; +} + +void scrollDown() +{ + manualScrolling = true; + + int totalHeight = 0; + for (int h : cachedHeights) + totalHeight += h; + + int visibleHeight = screen->getHeight() - (FONT_HEIGHT_SMALL * 2); + int maxScroll = totalHeight - visibleHeight; + if (maxScroll < 0) + maxScroll = 0; + + scrollY += 12; + if (scrollY > maxScroll) + scrollY = maxScroll; +} 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 === + // Step 1: Find tallest emote in the line int maxIconHeight = fontHeight; for (size_t i = 0; i < line.length();) { bool matched = false; @@ -81,25 +128,16 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string } } 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; + i += utf8CharLen(static_cast(line[i])); } } - // === Step 2: Baseline alignment === + // 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 === + // Step 3: Render line in segments size_t i = 0; bool inBold = false; @@ -148,10 +186,11 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string // Render the emote (if found) if (matchedEmote && i == nextEmotePos) { - int iconY = fontMidline - matchedEmote->height / 2 - 1; + int iconY = y + (lineHeight - matchedEmote->height) / 2; display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); cursorX += matchedEmote->width + 1; i += emojiLen; + continue; } else { // No more emotes — render the rest of the line std::string remaining = line.substr(i); @@ -164,235 +203,556 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string #else cursorX += display->getStringWidth(remaining.c_str()); #endif - break; } } } +// Reset scroll state when new messages arrive +void resetScrollState() +{ + scrollY = 0.0f; + scrollStarted = false; + waitingToReset = false; + scrollStartDelay = millis(); + lastTime = millis(); + manualScrolling = false; + didReset = false; +} + +void nudgeScroll(int8_t direction) +{ + if (direction == 0) + return; + + if (cachedHeights.empty()) { + scrollY = 0.0f; + return; + } + + OLEDDisplay *display = (screen != nullptr) ? screen->getDisplayDevice() : nullptr; + const int displayHeight = display ? display->getHeight() : 64; + const int navHeight = FONT_HEIGHT_SMALL; + const int usableHeight = std::max(0, displayHeight - navHeight); + + int totalHeight = 0; + for (int h : cachedHeights) + totalHeight += h; + + if (totalHeight <= usableHeight) { + scrollY = 0.0f; + return; + } + + const int scrollStop = std::max(0, totalHeight - usableHeight + cachedHeights.back()); + const int step = std::max(FONT_HEIGHT_SMALL, usableHeight / 3); + + float newScroll = scrollY + static_cast(direction) * static_cast(step); + if (newScroll < 0.0f) + newScroll = 0.0f; + if (newScroll > scrollStop) + newScroll = static_cast(scrollStop); + + if (newScroll != scrollY) { + scrollY = newScroll; + waitingToReset = false; + scrollStarted = false; + scrollStartDelay = millis(); + lastTime = millis(); + } +} + +// Fully free cached message data from heap +void clearMessageCache() +{ + std::vector().swap(cachedLines); + std::vector().swap(cachedHeights); + + // Reset scroll so we rebuild cleanly next time we enter the screen + resetScrollState(); +} + +// Current thread state +static ThreadMode currentMode = ThreadMode::ALL; +static int currentChannel = -1; +static uint32_t currentPeer = 0; + +// Registry of seen threads for manual toggle +static std::vector seenChannels; +static std::vector seenPeers; + +// Public helper so menus / store can clear stale registries +void clearThreadRegistries() +{ + seenChannels.clear(); + seenPeers.clear(); +} + +// Setter so other code can switch threads +void setThreadMode(ThreadMode mode, int channel /* = -1 */, uint32_t peer /* = 0 */) +{ + currentMode = mode; + currentChannel = channel; + currentPeer = peer; + didReset = false; // force reset when mode changes + + // Track channels we’ve seen + if (mode == ThreadMode::CHANNEL && channel >= 0) { + if (std::find(seenChannels.begin(), seenChannels.end(), channel) == seenChannels.end()) { + seenChannels.push_back(channel); + } + } + + // Track DMs we’ve seen + if (mode == ThreadMode::DIRECT && peer != 0) { + if (std::find(seenPeers.begin(), seenPeers.end(), peer) == seenPeers.end()) { + seenPeers.push_back(peer); + } + } +} + +ThreadMode getThreadMode() +{ + return currentMode; +} + +int getThreadChannel() +{ + return currentChannel; +} + +uint32_t getThreadPeer() +{ + return currentPeer; +} + +// Accessors for menuHandler +const std::vector &getSeenChannels() +{ + return seenChannels; +} +const std::vector &getSeenPeers() +{ + return seenPeers; +} + +static int centerYForRow(int y, int size) +{ + int midY = y + (FONT_HEIGHT_SMALL / 2); + return midY - (size / 2); +} + +// Helpers for drawing status marks (thickened strokes) +static void drawCheckMark(OLEDDisplay *display, int x, int y, int size) +{ + int topY = centerYForRow(y, size); + display->setColor(WHITE); + display->drawLine(x, topY + size / 2, x + size / 3, topY + size); + display->drawLine(x, topY + size / 2 + 1, x + size / 3, topY + size + 1); + display->drawLine(x + size / 3, topY + size, x + size, topY); + display->drawLine(x + size / 3, topY + size + 1, x + size, topY + 1); +} + +static void drawXMark(OLEDDisplay *display, int x, int y, int size = 8) +{ + int topY = centerYForRow(y, size); + display->setColor(WHITE); + display->drawLine(x, topY, x + size, topY + size); + display->drawLine(x, topY + 1, x + size, topY + size + 1); + display->drawLine(x + size, topY, x, topY + size); + display->drawLine(x + size, topY + 1, x, topY + size + 1); +} + +static void drawRelayMark(OLEDDisplay *display, int x, int y, int size = 8) +{ + int r = size / 2; + int centerY = centerYForRow(y, size) + r; + int centerX = x + r; + display->setColor(WHITE); + display->drawCircle(centerX, centerY, r); + display->drawLine(centerX, centerY - 2, centerX, centerY); + display->setPixel(centerX, centerY + 2); + display->drawLine(centerX - 1, centerY - 4, centerX + 1, centerY - 4); +} + +static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &line, const Emote *emotes, int emoteCount) +{ + std::string normalized = normalizeEmoji(line); + int totalWidth = 0; + + size_t i = 0; + while (i < normalized.length()) { + bool matched = false; + for (int e = 0; e < emoteCount; ++e) { + size_t emojiLen = strlen(emotes[e].label); + if (normalized.compare(i, emojiLen, emotes[e].label) == 0) { + totalWidth += emotes[e].width + 1; // +1 spacing + i += emojiLen; + matched = true; + break; + } + } + if (!matched) { + size_t charLen = utf8CharLen(static_cast(normalized[i])); +#if defined(OLED_UA) || defined(OLED_RU) + totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str(), charLen, true); +#else + totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str()); +#endif + i += charLen; + } + } + return totalWidth; +} + +struct MessageBlock { + size_t start; + size_t end; + bool mine; +}; + +static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool isHeaderLine) +{ + if (isHeaderLine) { + return lineTopY + (FONT_HEIGHT_SMALL - 1); + } + + int tallest = FONT_HEIGHT_SMALL; + for (int e = 0; e < numEmotes; ++e) { + if (line.find(emotes[e].label) != std::string::npos) { + if (emotes[e].height > tallest) + tallest = emotes[e].height; + } + } + + const int lineHeight = std::max(FONT_HEIGHT_SMALL, tallest); + const int iconTop = lineTopY + (lineHeight - tallest) / 2; + + return iconTop + tallest - 1; +} + +static std::vector buildMessageBlocks(const std::vector &isHeaderVec, const std::vector &isMineVec) +{ + std::vector blocks; + if (isHeaderVec.empty()) + return blocks; + + size_t start = 0; + bool mine = isMineVec[0]; + + for (size_t i = 1; i < isHeaderVec.size(); ++i) { + if (isHeaderVec[i]) { + MessageBlock b; + b.start = start; + b.end = i - 1; + b.mine = mine; + blocks.push_back(b); + + start = i; + mine = isMineVec[i]; + } + } + + MessageBlock last; + last.start = start; + last.end = isHeaderVec.size() - 1; + last.mine = mine; + blocks.push_back(last); + + return blocks; +} + +static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int totalHeight, int scrollOffset, int startY) +{ + if (totalHeight <= visibleHeight) + return; // no scrollbar needed + + int scrollbarX = display->getWidth() - 2; + int scrollbarHeight = visibleHeight; + int thumbHeight = std::max(6, (scrollbarHeight * visibleHeight) / totalHeight); + int maxScroll = std::max(1, totalHeight - visibleHeight); + int thumbY = startY + (scrollbarHeight - thumbHeight) * scrollOffset / maxScroll; + + for (int i = 0; i < thumbHeight; i++) { + display->setPixel(scrollbarX, thumbY + i); + } +} + void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + // Ensure any boot-relative timestamps are upgraded if RTC is valid + messageStore.upgradeBootRelativeTimestamps(); + + if (!didReset) { + resetScrollState(); + didReset = true; + } + // 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); + // Filter messages based on thread mode + std::deque filtered; + for (const auto &m : messageStore.getLiveMessages()) { + bool include = false; + switch (currentMode) { + case ThreadMode::ALL: + include = true; + break; + case ThreadMode::CHANNEL: + if (m.type == MessageType::BROADCAST && (int)m.channelIndex == currentChannel) + include = true; + break; + case ThreadMode::DIRECT: + if (m.dest != NODENUM_BROADCAST && (m.sender == currentPeer || m.dest == currentPeer)) + include = true; + break; + } + if (include) + filtered.push_back(m); + } display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); -#if defined(M5STACK_UNITC6L) - const int fixedTopHeight = 24; - const int windowX = 0; - const int windowY = fixedTopHeight; - const int windowWidth = 64; - const int windowHeight = SCREEN_HEIGHT - fixedTopHeight; -#else const int navHeight = FONT_HEIGHT_SMALL; const int scrollBottom = SCREEN_HEIGHT - navHeight; const int usableHeight = scrollBottom; - const int textWidth = SCREEN_WIDTH; + constexpr int LEFT_MARGIN = 2; + constexpr int RIGHT_MARGIN = 2; + constexpr int SCROLLBAR_WIDTH = 3; + constexpr int BUBBLE_PAD_X = 3; + constexpr int BUBBLE_PAD_Y = 4; + constexpr int BUBBLE_RADIUS = 4; + constexpr int BUBBLE_MIN_W = 24; + constexpr int BUBBLE_TEXT_INDENT = 2; -#endif - bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); - bool isBold = config.display.heading_bold; + // Derived widths + const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (BUBBLE_PAD_X * 2); + const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH; - // === Set Title + // Title string depending on mode + static char titleBuf[32]; const char *titleStr = "Messages"; + switch (currentMode) { + case ThreadMode::ALL: + titleStr = "Messages"; + break; + case ThreadMode::CHANNEL: { + const char *cname = channels.getName(currentChannel); + if (cname && cname[0]) { + snprintf(titleBuf, sizeof(titleBuf), "#%s", cname); + } else { + snprintf(titleBuf, sizeof(titleBuf), "Ch%d", currentChannel); + } + titleStr = titleBuf; + break; + } + case ThreadMode::DIRECT: { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer); + if (node && node->has_user) { + snprintf(titleBuf, sizeof(titleBuf), "@%s", node->user.short_name); + } else { + snprintf(titleBuf, sizeof(titleBuf), "@%08x", currentPeer); + } + titleStr = titleBuf; + break; + } + } - // 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 === + if (filtered.empty()) { + // If current conversation is empty go back to ALL view + if (currentMode != ThreadMode::ALL) { + setThreadMode(ThreadMode::ALL); + resetScrollState(); + return; // Next draw will rerun in ALL mode + } + + // Still in ALL mode and no messages at all → show placeholder graphics::drawCommonHeader(display, x, y, titleStr); + didReset = false; const char *messageString = "No messages"; int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2); -#if defined(M5STACK_UNITC6L) - display->drawString(center_text, windowY + (windowHeight / 2) - (FONT_HEIGHT_SMALL / 2) - 5, messageString); -#else display->drawString(center_text, getTextPositions(display)[2], messageString); -#endif graphics::drawCommonFooter(display, x, y); return; } - // === Header Construction === - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); - char headerStr[80]; - const char *sender = "???"; -#if defined(M5STACK_UNITC6L) - if (node && node->has_user) - sender = node->user.short_name; -#else - 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; - } - } -#endif - 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); + // Build lines for filtered messages (newest first) + std::vector allLines; + std::vector isMine; // track alignment + std::vector isHeader; // track header lines + std::vector ackForLine; - 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 { -#if defined(M5STACK_UNITC6L) - snprintf(headerStr, sizeof(headerStr), "%s from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(), - sender); -#else - snprintf(headerStr, sizeof(headerStr), "%s ago from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(), - sender); -#endif - } -#if defined(M5STACK_UNITC6L) - graphics::drawCommonHeader(display, x, y, titleStr); - int headerY = getTextPositions(display)[1]; - display->drawString(x, headerY, headerStr); - for (int separatorX = 0; separatorX < SCREEN_WIDTH; separatorX += 2) { - display->setPixel(separatorX, fixedTopHeight - 1); - } - cachedLines.clear(); - std::string fullMsg(messageBuf); - std::string currentLine; - for (size_t i = 0; i < fullMsg.size();) { - unsigned char c = fullMsg[i]; - size_t charLen = 1; - if ((c & 0xE0) == 0xC0) - charLen = 2; - else if ((c & 0xF0) == 0xE0) - charLen = 3; - else if ((c & 0xF8) == 0xF0) - charLen = 4; - std::string nextChar = fullMsg.substr(i, charLen); - std::string testLine = currentLine + nextChar; - if (display->getStringWidth(testLine.c_str()) > windowWidth) { - cachedLines.push_back(currentLine); - currentLine = nextChar; - } else { - currentLine = testLine; - } + for (auto it = filtered.rbegin(); it != filtered.rend(); ++it) { + const auto &m = *it; - i += charLen; - } - if (!currentLine.empty()) - cachedLines.push_back(currentLine); - cachedHeights = calculateLineHeights(cachedLines, emotes); - int yOffset = windowY; - int linesDrawn = 0; - for (size_t i = 0; i < cachedLines.size(); ++i) { - if (linesDrawn >= 2) - break; - int lineHeight = cachedHeights[i]; - if (yOffset + lineHeight > windowY + windowHeight) - break; - drawStringWithEmotes(display, windowX, yOffset, cachedLines[i], emotes, numEmotes); - yOffset += lineHeight; - linesDrawn++; - } - screen->forceDisplay(); -#else - uint32_t now = millis(); -#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) - - 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 = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) { - display->setPixel(separatorX, headerY + ((isHighResolution) ? 19 : 13)); + // Channel / destination labeling + char chanType[32] = ""; + if (currentMode == ThreadMode::ALL) { + if (m.dest == NODENUM_BROADCAST) { + const char *name = channels.getName(m.channelIndex); + if (currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) { + if (strcmp(name, "ShortTurbo") == 0) + name = "ShortT"; + else if (strcmp(name, "ShortSlow") == 0) + name = "ShortS"; + else if (strcmp(name, "ShortFast") == 0) + name = "ShortF"; + else if (strcmp(name, "MediumSlow") == 0) + name = "MedS"; + else if (strcmp(name, "MediumFast") == 0) + name = "MedF"; + else if (strcmp(name, "LongSlow") == 0) + name = "LongS"; + else if (strcmp(name, "LongFast") == 0) + name = "LongF"; + else if (strcmp(name, "LongTurbo") == 0) + name = "LongT"; + else if (strcmp(name, "LongMod") == 0) + name = "LongM"; + } + snprintf(chanType, sizeof(chanType), "#%s", name); + } else { + snprintf(chanType, sizeof(chanType), "(DM)"); } + } - // Center the emote below the header line + separator + nav - int remainingHeight = SCREEN_HEIGHT - (headerY + FONT_HEIGHT_SMALL) - navHeight; - int emoteY = headerY + 6 + FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange; - display->drawXbm((SCREEN_WIDTH - e.width) / 2, emoteY, e.width, e.height, e.bitmap); + // Calculate how long ago + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + uint32_t seconds = 0; + bool invalidTime = true; - // Draw header at the end to sort out overlapping elements - graphics::drawCommonHeader(display, x, y, titleStr); - return; + if (m.timestamp > 0 && nowSecs > 0) { + if (nowSecs >= m.timestamp) { + seconds = nowSecs - m.timestamp; + invalidTime = (seconds > 315360000); // >10 years + } else { + uint32_t ahead = m.timestamp - nowSecs; + if (ahead <= 600) { // allow small skew + seconds = 0; + invalidTime = false; + } + } + } else if (m.timestamp > 0 && nowSecs == 0) { + // RTC not valid: only trust boot-relative if same boot + uint32_t bootNow = millis() / 1000; + if (m.isBootRelative && m.timestamp <= bootNow) { + seconds = bootNow - m.timestamp; + invalidTime = false; + } else { + invalidTime = true; // old persisted boot-relative, ignore until healed + } + } + + char timeBuf[16]; + if (invalidTime) { + snprintf(timeBuf, sizeof(timeBuf), "???"); + } else if (seconds < 60) { + snprintf(timeBuf, sizeof(timeBuf), "%us", seconds); + } else if (seconds < 3600) { + snprintf(timeBuf, sizeof(timeBuf), "%um", seconds / 60); + } else if (seconds < 86400) { + snprintf(timeBuf, sizeof(timeBuf), "%uh", seconds / 3600); + } else { + snprintf(timeBuf, sizeof(timeBuf), "%ud", seconds / 86400); + } + + // Build header line for this message + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(m.sender); + meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest); + + char senderBuf[48] = ""; + if (node && node->has_user) { + // Use long name if present + strncpy(senderBuf, node->user.long_name, sizeof(senderBuf) - 1); + senderBuf[sizeof(senderBuf) - 1] = '\0'; + } else { + // No long/short name → show NodeID in parentheses + snprintf(senderBuf, sizeof(senderBuf), "(%08x)", m.sender); + } + + // If this is *our own* message, override senderBuf to who the recipient was + bool mine = (m.sender == nodeDB->getNodeNum()); + if (mine && node_recipient && node_recipient->has_user) { + strcpy(senderBuf, node_recipient->user.long_name); + } + + // Shrink Sender name if needed + int availWidth = (mine ? rightTextWidth : leftTextWidth) - display->getStringWidth(timeBuf) - + display->getStringWidth(chanType) - display->getStringWidth(" @..."); + if (availWidth < 0) + availWidth = 0; + + size_t origLen = strlen(senderBuf); + while (senderBuf[0] && display->getStringWidth(senderBuf) > availWidth) { + senderBuf[strlen(senderBuf) - 1] = '\0'; + } + + // If we actually truncated, append "..." + if (strlen(senderBuf) < origLen) { + strcat(senderBuf, "..."); + } + + // Final header line + char headerStr[96]; + if (mine) { + if (currentMode == ThreadMode::ALL) { + if (strcmp(chanType, "(DM)") == 0) { + snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, senderBuf); + } else { + snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, chanType); + } + } else { + snprintf(headerStr, sizeof(headerStr), "%s", timeBuf); + } + } else { + snprintf(headerStr, sizeof(headerStr), "%s @%s %s", timeBuf, senderBuf, chanType); + } + + // Push header line + allLines.push_back(std::string(headerStr)); + isMine.push_back(mine); + isHeader.push_back(true); + ackForLine.push_back(m.ackStatus); + + const char *msgText = MessageStore::getText(m); + + int wrapWidth = mine ? rightTextWidth : leftTextWidth; + std::vector wrapped = generateLines(display, "", msgText, wrapWidth); + for (auto &ln : wrapped) { + allLines.push_back(ln); + isMine.push_back(mine); + isHeader.push_back(false); + ackForLine.push_back(AckStatus::NONE); } } -#endif - // === Generate the cache key === - size_t currentKey = (size_t)mp.from; - currentKey ^= ((size_t)mp.to << 8); - currentKey ^= ((size_t)mp.rx_time << 16); - currentKey ^= ((size_t)mp.id << 24); - if (cachedKey != currentKey) { - LOG_INFO("Onscreen message scroll cache key needs updating: cachedKey=0x%0x, currentKey=0x%x", cachedKey, currentKey); + // Cache lines and heights + cachedLines = allLines; + cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader); - // Cache miss - regenerate lines and heights - cachedLines = generateLines(display, headerStr, messageBuf, textWidth); - cachedHeights = calculateLineHeights(cachedLines, emotes); - cachedKey = currentKey; - } else { - // Cache hit but update the header line with current time information - cachedLines[0] = std::string(headerStr); - // The header always has a fixed height since it doesn't contain emotes - // As per calculateLineHeights logic for lines without emotes: - cachedHeights[0] = FONT_HEIGHT_SMALL - 2; - if (cachedHeights[0] < 8) - cachedHeights[0] = 8; // minimum safety - } + std::vector blocks = buildMessageBlocks(isHeader, isMine); - // === Scrolling logic === + // Scrolling logic (unchanged) int totalHeight = 0; - for (size_t i = 1; i < cachedHeights.size(); ++i) { + for (size_t i = 0; i < cachedHeights.size(); ++i) totalHeight += cachedHeights[i]; - } - int usableScrollHeight = usableHeight - cachedHeights[0]; // remove header height + int usableScrollHeight = usableHeight; int scrollStop = std::max(0, totalHeight - usableScrollHeight + cachedHeights.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. +#ifndef USE_EINK + uint32_t now = millis(); float delta = (now - lastTime) / 400.0f; lastTime = now; + const float scrollSpeed = 2.0f; - 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 (!manualScrolling && totalHeight > usableScrollHeight) { if (scrollStarted) { if (!waitingToReset) { scrollY += delta * scrollSpeed; @@ -408,29 +768,227 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 scrollStartDelay = lastTime; } } - } else { + } else if (!manualScrolling) { scrollY = 0; } +#else + // E-Ink: disable autoscroll + scrollY = 0.0f; + waitingToReset = false; + scrollStarted = false; + lastTime = millis(); +#endif - int scrollOffset = static_cast(scrollY); - int yOffset = -scrollOffset + getTextPositions(display)[1]; - for (int separatorX = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) { - display->setPixel(separatorX, yOffset + ((isHighResolution) ? 19 : 13)); + int finalScroll = (int)scrollY; + int yOffset = -finalScroll + getTextPositions(display)[1]; + const int contentTop = getTextPositions(display)[1]; + const int contentBottom = scrollBottom; // already excludes nav line + const int rightEdge = SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN; + const int bubbleGapY = std::max(1, MESSAGE_BLOCK_GAP / 2); + + std::vector lineTop; + lineTop.resize(cachedLines.size()); + { + int acc = 0; + for (size_t i = 0; i < cachedLines.size(); ++i) { + lineTop[i] = yOffset + acc; + acc += cachedHeights[i]; + } } - // === Render visible lines === - renderMessageContent(display, cachedLines, cachedHeights, x, yOffset, scrollBottom, emotes, numEmotes, isInverted, isBold); + // Draw bubbles + for (size_t bi = 0; bi < blocks.size(); ++bi) { + const auto &b = blocks[bi]; + if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end) + continue; - // Draw header at the end to sort out overlapping elements + int visualTop = lineTop[b.start]; + + int topY; + if (isHeader[b.start]) { + // Header start + constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2 + topY = visualTop - BUBBLE_PAD_TOP_HEADER; + } else { + // Body start + bool thisLineHasEmote = false; + for (int e = 0; e < numEmotes; ++e) { + if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) { + thisLineHasEmote = true; + break; + } + } + if (thisLineHasEmote) { + constexpr int EMOTE_PADDING_ABOVE = 4; + visualTop -= EMOTE_PADDING_ABOVE; + } + topY = visualTop - BUBBLE_PAD_Y; + } + int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]); + int bottomY = visualBottom + BUBBLE_PAD_Y; + + if (bi + 1 < blocks.size()) { + int nextHeaderIndex = (int)blocks[bi + 1].start; + int nextTop = lineTop[nextHeaderIndex]; + int maxBottom = nextTop - 1 - bubbleGapY; + if (bottomY > maxBottom) + bottomY = maxBottom; + } + + if (bottomY <= topY + 2) + continue; + + if (bottomY < contentTop || topY > contentBottom - 1) + continue; + + int maxLineW = 0; + + for (size_t i = b.start; i <= b.end; ++i) { + int w = 0; + if (isHeader[i]) { + w = display->getStringWidth(cachedLines[i].c_str()); + if (b.mine) + w += 12; // room for ACK/NACK/relay mark + } else { + w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); + } + if (w > maxLineW) + maxLineW = w; + } + + int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (BUBBLE_PAD_X * 2)); + int bubbleH = (bottomY - topY) + 1; + int bubbleX = 0; + if (b.mine) { + bubbleX = rightEdge - bubbleW; + } else { + bubbleX = x; + } + if (bubbleX < x) + bubbleX = x; + if (bubbleX + bubbleW > rightEdge) + bubbleW = std::max(1, rightEdge - bubbleX); + + if (bubbleW > 1 && bubbleH > 1) { + int x1 = bubbleX + bubbleW - 1; + int y1 = topY + bubbleH - 1; + + if (b.mine) { + // Send Message (Right side) + display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH); + // Top Right Corner + display->drawRect(x1 - 1, topY, 2, 1); + display->drawRect(x1, topY, 1, 2); + // Bottom Right Corner + display->drawRect(x1 - 1, bottomY - 2, 2, 1); + display->drawRect(x1, bottomY - 3, 1, 2); + // Knock the corners off to make a bubble + display->setColor(BLACK); + display->drawRect(x1 - bubbleW + 2, topY - 1, 1, 1); + display->drawRect(x1 - bubbleW + 2, bottomY - 1, 1, 1); + display->setColor(WHITE); + } else { + // Received Message (Left Side) + display->drawRect(bubbleX, topY, bubbleW + 1, bubbleH); + // Top Left Corner + display->drawRect(bubbleX + 1, topY + 1, 2, 1); + display->drawRect(bubbleX + 1, topY + 1, 1, 2); + // Bottom Left Corner + display->drawRect(bubbleX + 1, bottomY - 1, 2, 1); + display->drawRect(bubbleX + 1, bottomY - 2, 1, 2); + // Knock the corners off to make a bubble + display->setColor(BLACK); + display->drawRect(bubbleX + bubbleW, topY, 1, 1); + display->drawRect(bubbleX + bubbleW, bottomY, 1, 1); + display->setColor(WHITE); + } + } + } + + // Render visible lines + int lineY = yOffset; + for (size_t i = 0; i < cachedLines.size(); ++i) { + + if (lineY > -cachedHeights[i] && lineY < scrollBottom) { + if (isHeader[i]) { + + int w = display->getStringWidth(cachedLines[i].c_str()); + int headerX; + if (isMine[i]) { + // push header left to avoid overlap with scrollbar + headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - BUBBLE_TEXT_INDENT; + if (headerX < LEFT_MARGIN) + headerX = LEFT_MARGIN; + } else { + headerX = x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT; + } + display->drawString(headerX, lineY, cachedLines[i].c_str()); + + // Draw underline just under header text + int underlineY = lineY + FONT_HEIGHT_SMALL; + + int underlineW = w; + int maxW = rightEdge - headerX; + if (maxW < 0) + maxW = 0; + if (underlineW > maxW) + underlineW = maxW; + + for (int px = 0; px < underlineW; ++px) { + display->setPixel(headerX + px, underlineY); + } + + // Draw ACK/NACK mark for our own messages + if (isMine[i]) { + int markX = headerX - 10; + int markY = lineY; + if (ackForLine[i] == AckStatus::ACKED) { + // Destination ACK + drawCheckMark(display, markX, markY, 8); + } else if (ackForLine[i] == AckStatus::NACKED || ackForLine[i] == AckStatus::TIMEOUT) { + // Failure or timeout + drawXMark(display, markX, markY, 8); + } else if (ackForLine[i] == AckStatus::RELAYED) { + // Relay ACK + drawRelayMark(display, markX, markY, 8); + } + // AckStatus::NONE → show nothing + } + + } else { + // Render message line + if (isMine[i]) { + // Calculate actual rendered width including emotes + int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); + int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - BUBBLE_TEXT_INDENT; + if (rightX < LEFT_MARGIN) + rightX = LEFT_MARGIN; + + drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes); + } else { + drawStringWithEmotes(display, x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT, lineY, cachedLines[i], emotes, + numEmotes); + } + } + } + + lineY += cachedHeights[i]; + } + + // Draw scrollbar + drawMessageScrollbar(display, usableHeight, totalHeight, finalScroll, getTextPositions(display)[1]); graphics::drawCommonHeader(display, x, y, titleStr); -#endif graphics::drawCommonFooter(display, x, y); } std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth) { std::vector lines; - lines.push_back(std::string(headerStr)); // Header line is always first + + // Only push headerStr if it's not empty (prevents extra blank line after headers) + if (headerStr && headerStr[0] != '\0') { + lines.push_back(std::string(headerStr)); + } std::string line, word; for (int i = 0; messageBuf[i]; ++i) { @@ -453,10 +1011,6 @@ std::vector generateLines(OLEDDisplay *display, const char *headerS } else { word += ch; std::string test = line + word; -// Keep these lines for diagnostics -// LOG_INFO("Char: '%c' (0x%02X)", ch, (unsigned char)ch); -// LOG_INFO("Current String: %s", test.c_str()); -// Note: there are boolean comparison uint16 (getStringWidth) with int (textWidth), hope textWidth is always positive :) #if defined(OLED_UA) || defined(OLED_RU) uint16_t strWidth = display->getStringWidth(test.c_str(), test.length(), true); #else @@ -478,28 +1032,69 @@ std::vector generateLines(OLEDDisplay *display, const char *headerS return lines; } - -std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes) +std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, + const std::vector &isHeaderVec) { + // Tunables for layout control + constexpr int HEADER_UNDERLINE_GAP = 0; // space between underline and first body line + constexpr int HEADER_UNDERLINE_PIX = 1; // underline thickness (1px row drawn) + constexpr int BODY_LINE_LEADING = -4; // default vertical leading for normal body lines + constexpr int EMOTE_PADDING_ABOVE = 4; // space above emote line (added to line above) + constexpr int EMOTE_PADDING_BELOW = 3; // space below emote line (added to emote line) + std::vector rowHeights; + rowHeights.reserve(lines.size()); - for (const auto &_line : lines) { - int lineHeight = FONT_HEIGHT_SMALL; + for (size_t idx = 0; idx < lines.size(); ++idx) { + const auto &line = lines[idx]; + const int baseHeight = FONT_HEIGHT_SMALL; + int lineHeight = baseHeight; + + // Detect if THIS line or NEXT line contains an emote bool hasEmote = false; - + int tallestEmote = baseHeight; 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); + if (line.find(emotes[i].label) != std::string::npos) { hasEmote = true; + tallestEmote = std::max(tallestEmote, emotes[i].height); } } - // 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 + bool nextHasEmote = false; + if (idx + 1 < lines.size()) { + for (int i = 0; i < numEmotes; ++i) { + if (lines[idx + 1].find(emotes[i].label) != std::string::npos) { + nextHasEmote = true; + break; + } + } + } + + if (isHeaderVec[idx]) { + // Header line spacing + lineHeight = baseHeight + HEADER_UNDERLINE_PIX + HEADER_UNDERLINE_GAP; + } else { + // Base spacing for normal lines + int desiredBody = baseHeight + BODY_LINE_LEADING; + + if (hasEmote) { + // Emote line: add overshoot + bottom padding + int overshoot = std::max(0, tallestEmote - baseHeight); + lineHeight = desiredBody + overshoot + EMOTE_PADDING_BELOW; + } else { + // Regular line: no emote → standard spacing + lineHeight = desiredBody; + + // If next line has an emote → add top padding *here* + if (nextHasEmote) { + lineHeight += EMOTE_PADDING_ABOVE; + } + } + + // Add block gap if next is a header + if (idx + 1 < lines.size() && isHeaderVec[idx + 1]) { + lineHeight += MESSAGE_BLOCK_GAP; + } } rowHeights.push_back(lineHeight); @@ -508,22 +1103,125 @@ std::vector calculateLineHeights(const std::vector &lines, con return rowHeights; } -void renderMessageContent(OLEDDisplay *display, const std::vector &lines, const std::vector &rowHeights, int x, - int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold) +void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const meshtastic_MeshPacket &packet) { - 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, lineY, lines[i].c_str()); - if (isBold) - display->drawString(x, lineY, lines[i].c_str()); - } else { - drawStringWithEmotes(display, x, lineY, lines[i], emotes, numEmotes); + if (packet.from != 0) { + hasUnreadMessage = true; + + // Determine if message belongs to a muted channel + bool isChannelMuted = false; + if (sm.type == MessageType::BROADCAST) { + const meshtastic_Channel channel = channels.getByIndex(packet.channel ? packet.channel : channels.getPrimaryIndex()); + if (channel.settings.has_module_settings && channel.settings.module_settings.is_muted) + isChannelMuted = true; + } + + // Banner logic + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from); + char longName[48] = "?"; + if (node && node->user.long_name) { + strncpy(longName, node->user.long_name, sizeof(longName) - 1); + longName[sizeof(longName) - 1] = '\0'; + } + int availWidth = display->getWidth() - ((currentResolution == ScreenResolution::High) ? 40 : 20); + if (availWidth < 0) + availWidth = 0; + + size_t origLen = strlen(longName); + while (longName[0] && display->getStringWidth(longName) > availWidth) { + longName[strlen(longName) - 1] = '\0'; + } + if (strlen(longName) < origLen) { + strcat(longName, "..."); + } + const char *msgRaw = reinterpret_cast(packet.decoded.payload.bytes); + + char banner[256]; + bool isAlert = false; + + // Check if alert detection is enabled via external notification module + if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra || + moduleConfig.external_notification.alert_bell_buzzer) { + 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 { + // Skip muted channels unless it's an alert + if (isChannelMuted) + return; + + if (longName && longName[0]) { + if (currentResolution == ScreenResolution::UltraLow) { + strcpy(banner, "New Message"); + } else { + snprintf(banner, sizeof(banner), "New Message from\n%s", longName); + } + } else + strcpy(banner, "New Message"); + } + + // Append context (which channel or DM) so the banner shows where the message arrived + { + char contextBuf[64] = ""; + if (sm.type == MessageType::BROADCAST) { + const char *cname = channels.getName(sm.channelIndex); + if (cname && cname[0]) + snprintf(contextBuf, sizeof(contextBuf), "in #%s", cname); + else + snprintf(contextBuf, sizeof(contextBuf), "in Ch%d", sm.channelIndex); + } + + if (contextBuf[0]) { + size_t cur = strlen(banner); + if (cur + 1 < sizeof(banner)) { + if (cur > 0 && banner[cur - 1] != '\n') { + banner[cur] = '\n'; + banner[cur + 1] = '\0'; + cur++; + } + strncat(banner, contextBuf, sizeof(banner) - cur - 1); + } + } + } + + // Shorter banner if already in a conversation (Channel or Direct) + bool inThread = (getThreadMode() != ThreadMode::ALL); + + if (shouldWakeOnReceivedMessage()) { + screen->setOn(true); + } + + screen->showSimpleBanner(banner, inThread ? 1000 : 3000); + } + + // Always focus into the correct conversation thread when a message with real text arrives + const char *msgText = MessageStore::getText(sm); + if (msgText && msgText[0] != '\0') { + setThreadFor(sm, packet); + } + + // Reset scroll for a clean start + resetScrollState(); +} + +void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet) +{ + if (packet.to == 0 || packet.to == NODENUM_BROADCAST) { + setThreadMode(ThreadMode::CHANNEL, sm.channelIndex); + } else { + uint32_t localNode = nodeDB->getNodeNum(); + uint32_t peer = (sm.sender == localNode) ? packet.to : sm.sender; + setThreadMode(ThreadMode::DIRECT, -1, peer); } } diff --git a/src/graphics/draw/MessageRenderer.h b/src/graphics/draw/MessageRenderer.h index c15a699f7..7dec6adec 100644 --- a/src/graphics/draw/MessageRenderer.h +++ b/src/graphics/draw/MessageRenderer.h @@ -1,7 +1,11 @@ #pragma once +#include "MessageStore.h" // for StoredMessage +#if HAS_SCREEN #include "OLEDDisplay.h" #include "OLEDDisplayUi.h" #include "graphics/emotes.h" +#include "mesh/generated/meshtastic/mesh.pb.h" // for meshtastic_MeshPacket +#include #include #include @@ -10,6 +14,27 @@ namespace graphics namespace MessageRenderer { +// Thread filter modes +enum class ThreadMode { ALL, CHANNEL, DIRECT }; + +// Setter for switching thread mode +void setThreadMode(ThreadMode mode, int channel = -1, uint32_t peer = 0); + +// Getter for current mode +ThreadMode getThreadMode(); + +// Getter for current channel (valid if mode == CHANNEL) +int getThreadChannel(); + +// Getter for current peer (valid if mode == DIRECT) +uint32_t getThreadPeer(); + +// Registry accessors for menuHandler +const std::vector &getSeenChannels(); +const std::vector &getSeenPeers(); + +void clearThreadRegistries(); + // Text and emote rendering void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount); @@ -20,11 +45,27 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth); // Function to calculate heights for each line -std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes); +std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, + const std::vector &isHeaderVec); -// Function to render the message content -void renderMessageContent(OLEDDisplay *display, const std::vector &lines, const std::vector &rowHeights, int x, - int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold); +// Reset scroll state when new messages arrive +void resetScrollState(); + +// Manual scroll control for encoder-style inputs +void nudgeScroll(int8_t direction); + +// Helper to auto-select the correct thread mode from a message +void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet); + +// Handles a new incoming/outgoing message: banner, wake, thread select, scroll reset +void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const meshtastic_MeshPacket &packet); + +// Clear Message Line Cache from Message Renderer +void clearMessageCache(); + +void scrollUp(); +void scrollDown(); } // namespace MessageRenderer } // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 1a36a6188..9d6780130 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -23,7 +23,6 @@ extern graphics::Screen *screen; #if defined(M5STACK_UNITC6L) static uint32_t lastSwitchTime = 0; -#else #endif namespace graphics { @@ -46,79 +45,119 @@ void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t * } // Static variables for dynamic cycling -static NodeListMode currentMode = MODE_LAST_HEARD; +static ListMode_Node currentMode_Nodes = MODE_LAST_HEARD; +static ListMode_Location currentMode_Location = MODE_DISTANCE; static int scrollIndex = 0; +// Popup overlay state +static uint32_t popupTime = 0; +static int popupTotal = 0; +static int popupStart = 0; +static int popupEnd = 0; +static int popupPage = 1; +static int popupMaxPage = 1; + +static const uint32_t POPUP_DURATION_MS = 1000; // 1 second visible + +// ============================= +// Scrolling Logic +// ============================= +void scrollUp() +{ + if (scrollIndex > 0) + scrollIndex--; + + popupTime = millis(); // show popup +} + +void scrollDown() +{ + scrollIndex++; + popupTime = millis(); +} // ============================= // Utility Functions // ============================= -const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node) +const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth) { - const char *name = NULL; - static char nodeName[16] = "?"; - if (config.display.use_long_node_name == true) { - if (node->has_user && strlen(node->user.long_name) > 0) { - name = node->user.long_name; - } else { - snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF)); - } - } else { - if (node->has_user && strlen(node->user.short_name) > 0) { - name = node->user.short_name; - } else { - snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF)); - } + static char nodeName[25]; // single static buffer we return + nodeName[0] = '\0'; + + auto writeFallbackId = [&] { + std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast(node ? (node->num & 0xFFFF) : 0)); + }; + + // 1) Choose target candidate (long vs short) only if present + const char *raw = nullptr; + if (node && node->has_user) { + raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name; } - // Use sanitizeString() function and copy directly into nodeName - std::string sanitized_name = sanitizeString(name ? name : ""); + // 2) Sanitize (empty if raw is null/empty) + std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{}; - if (!sanitized_name.empty()) { - strncpy(nodeName, sanitized_name.c_str(), sizeof(nodeName) - 1); - nodeName[sizeof(nodeName) - 1] = '\0'; + // 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed) + if (s.empty() || s == "¿" || s.find_first_not_of("¿") == std::string::npos) { + writeFallbackId(); } else { - snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF)); + // %.*s ensures null-termination and safe truncation to buffer size - 1 + std::snprintf(nodeName, sizeof(nodeName), "%.*s", static_cast(sizeof(nodeName) - 1), s.c_str()); } - if (config.display.use_long_node_name == true) { - int availWidth = (SCREEN_WIDTH / 2) - 65; + // 4) Width-based truncation + ellipsis (long-name mode only) + if (config.display.use_long_node_name && display) { + int availWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38); if (availWidth < 0) availWidth = 0; - size_t origLen = strlen(nodeName); - while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) { - nodeName[strlen(nodeName) - 1] = '\0'; + const size_t beforeLen = std::strlen(nodeName); + + // Trim from the end until it fits or is empty + size_t len = beforeLen; + while (len && display->getStringWidth(nodeName) > availWidth) { + nodeName[--len] = '\0'; } - // If we actually truncated, append "..." (ensure space remains in buffer) - if (strlen(nodeName) < origLen) { - size_t len = strlen(nodeName); - size_t maxLen = sizeof(nodeName) - 4; // 3 for "..." and 1 for '\0' - if (len > maxLen) { - nodeName[maxLen] = '\0'; - len = maxLen; + // If truncated, append "..." (respect buffer size) + if (len < beforeLen) { + // Make sure there's room for "..." and '\0' + const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0' + const size_t needed = 3; // "..." + if (len > capForText - needed) { + len = capForText - needed; + nodeName[len] = '\0'; } - strcat(nodeName, "..."); + std::strcat(nodeName, "..."); } } return nodeName; } -const char *getCurrentModeTitle(int screenWidth) +const char *getCurrentModeTitle_Nodes(int screenWidth) { - switch (currentMode) { + switch (currentMode_Nodes) { case MODE_LAST_HEARD: return "Last Heard"; case MODE_HOP_SIGNAL: #ifdef USE_EINK return "Hops/Sig"; #else - return (isHighResolution) ? "Hops/Signal" : "Hops/Sig"; + return (currentResolution == ScreenResolution::High) ? "Hops/Signal" : "Hops/Sig"; #endif + default: + return "Nodes"; + } +} + +const char *getCurrentModeTitle_Location(int screenWidth) +{ + switch (currentMode_Location) { case MODE_DISTANCE: return "Distance"; + case MODE_BEARING: + return "Bearings"; default: return "Nodes"; } @@ -137,10 +176,9 @@ int calculateMaxScroll(int totalEntries, int visibleRows) void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { - int columnWidth = display->getWidth() / 2; - int separatorX = x + columnWidth - 2; + x = (currentResolution == ScreenResolution::High) ? x - 2 : (currentResolution == ScreenResolution::Low) ? x - 1 : x; for (int y = yStart; y <= yEnd; y += 2) { - display->setPixel(separatorX, y); + display->setPixel(x, y); } } @@ -152,7 +190,8 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, 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 perPage = visibleNodeRows * columns; + int maxScroll = std::max(0, (totalEntries - 1) / perPage); int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll); for (int i = 0; i < thumbHeight; i++) { @@ -167,9 +206,11 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); - int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); + int nameMaxWidth = columnWidth - 25; + int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); - const char *nodeName = getSafeNodeName(display, node); + const char *nodeName = getSafeNodeName(display, node, columnWidth); + bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; char timeStr[10]; uint32_t seconds = sinceLastSeen(node); @@ -188,14 +229,21 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawString(x + ((isHighResolution) ? 6 : 3), y, nodeName); + display->drawString(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nodeName); if (node->is_favorite) { - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } + if (node->is_ignored || isMuted) { + if (currentResolution == ScreenResolution::High) { + display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); + } else { + display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); + } + } 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 @@ -209,24 +257,32 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = columnWidth - 25; - int barsOffset = (isHighResolution) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); - int hopOffset = (isHighResolution) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); + int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); + int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); int barsXOffset = columnWidth - barsOffset; - const char *nodeName = getSafeNodeName(display, node); + const char *nodeName = getSafeNodeName(display, node, columnWidth); + bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName); + display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); if (node->is_favorite) { - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } + if (node->is_ignored || isMuted) { + if (currentResolution == ScreenResolution::High) { + display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); + } else { + display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); + } + } // Draw signal strength bars int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; @@ -256,9 +312,11 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); - int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + int nameMaxWidth = + columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); - const char *nodeName = getSafeNodeName(display, node); + const char *nodeName = getSafeNodeName(display, node, columnWidth); + bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; char distStr[10] = ""; meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); @@ -311,36 +369,41 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName); + display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); if (node->is_favorite) { - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } + if (node->is_ignored || isMuted) { + if (currentResolution == ScreenResolution::High) { + display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); + } else { + display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); + } + } if (strlen(distStr) > 0) { - int offset = (isHighResolution) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) + int offset = (currentResolution == ScreenResolution::High) + ? (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) +void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { - switch (currentMode) { + switch (currentMode_Nodes) { 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; } @@ -351,20 +414,29 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 bool isLeftCol = (x < SCREEN_WIDTH / 2); // Adjust max text width depending on column and screen width - int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + int nameMaxWidth = + columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); - const char *nodeName = getSafeNodeName(display, node); + const char *nodeName = getSafeNodeName(display, node, columnWidth); + bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName); + display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); if (node->is_favorite) { - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } + if (node->is_ignored || isMuted) { + if (currentResolution == ScreenResolution::High) { + display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); + } else { + display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); + } + } } void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, @@ -374,7 +446,7 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 return; bool isLeftCol = (x < SCREEN_WIDTH / 2); - int arrowXOffset = (isHighResolution) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); + int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); int centerX = x + columnWidth - arrowXOffset; int centerY = y + FONT_HEIGHT_SMALL / 2; @@ -431,11 +503,6 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t locationScreen = true; else if (strcmp(title, "Distance") == 0) locationScreen = true; -#if defined(M5STACK_UNITC6L) - int columnWidth = display->getWidth(); -#else - int columnWidth = display->getWidth() / 2; -#endif display->clear(); // Draw the battery/time header @@ -444,39 +511,74 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t // Space below header y += COMMON_HEADER_HEIGHT; + int totalColumns = 1; // Default to 1 column + + if (config.display.use_long_node_name) { + if (SCREEN_WIDTH <= 240) { + totalColumns = 1; + } else if (SCREEN_WIDTH > 240) { + totalColumns = 2; + } + } else { + if (SCREEN_WIDTH <= 64) { + totalColumns = 1; + } else if (SCREEN_WIDTH > 64 && SCREEN_WIDTH <= 240) { + totalColumns = 2; + } else { + totalColumns = 3; + } + } + + int columnWidth = display->getWidth() / totalColumns; + int totalEntries = nodeDB->getNumMeshNodes(); int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; int numskipped = 0; int visibleNodeRows = totalRowsAvailable; -#if defined(M5STACK_UNITC6L) - int totalColumns = 1; -#else - int totalColumns = 2; -#endif - int startIndex = scrollIndex * visibleNodeRows * totalColumns; - if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) { - startIndex++; // skip own node - } - int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); + // Build filtered + ordered list + std::vector drawList; + drawList.reserve(totalEntries); + for (int i = 0; i < totalEntries; i++) { + auto *n = nodeDB->getMeshNodeByIndex(i); + + if (!n) + continue; + if (n->num == nodeDB->getNodeNum()) + continue; + if (locationScreen && !n->has_position) + continue; + + drawList.push_back(n->num); + } + totalEntries = drawList.size(); + int perPage = visibleNodeRows * totalColumns; + + int maxScroll = 0; + if (perPage > 0) { + maxScroll = std::max(0, (totalEntries - 1) / perPage); + } + + if (scrollIndex > maxScroll) + scrollIndex = maxScroll; + 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) { - if (locationScreen && !nodeDB->getMeshNodeByIndex(i)->has_position) { - numskipped++; - continue; - } + for (int idx = startIndex; idx < endIndex; idx++) { + uint32_t nodeNum = drawList[idx]; + auto *node = nodeDB->getMeshNode(nodeNum); int xPos = x + (col * columnWidth); int yPos = y + yOffset; - renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth); - if (extras) { - extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon); - } + renderer(display, node, xPos, yPos, columnWidth); + + if (extras) + extras(display, node, xPos, yPos, columnWidth, heading, lat, lon); lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); yOffset += rowYOffset; @@ -495,17 +597,73 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t // This should correct the scrollbar totalEntries -= numskipped; -#if !defined(M5STACK_UNITC6L) // Draw column separator - if (shownCount > 0) { + if (currentResolution != ScreenResolution::UltraLow && shownCount > 0) { const int firstNodeY = y + 3; - drawColumnSeparator(display, x, firstNodeY, lastNodeY); + for (int horizontal_offset = 1; horizontal_offset < totalColumns; horizontal_offset++) { + drawColumnSeparator(display, columnWidth * horizontal_offset, firstNodeY, lastNodeY); + } } -#endif const int scrollStartY = y + 3; - drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); + drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, totalColumns, scrollStartY); graphics::drawCommonFooter(display, x, y); + + // Scroll Popup Overlay + if (millis() - popupTime < POPUP_DURATION_MS) { + popupTotal = totalEntries; + + int perPage = visibleNodeRows * totalColumns; + + popupStart = startIndex + 1; + popupEnd = std::min(startIndex + perPage, totalEntries); + + popupPage = (scrollIndex + 1); + popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage); + + char buf[32]; + snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage); + + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // Box padding + int padding = 2; + int textW = display->getStringWidth(buf); + int textH = FONT_HEIGHT_SMALL; + int boxWidth = textW + padding * 3; + int boxHeight = textH + padding * 2; + + // Center of usable screen area: + int headerHeight = FONT_HEIGHT_SMALL - 1; + int footerHeight = FONT_HEIGHT_SMALL + 2; + + int usableTop = headerHeight; + int usableBottom = display->getHeight() - footerHeight; + int usableHeight = usableBottom - usableTop; + + // Center point inside usable area + int boxLeft = (display->getWidth() - boxWidth) / 2; + int boxTop = usableTop + (usableHeight - boxHeight) / 2; + + // Draw Box + display->setColor(BLACK); + display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); + display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); + display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); + display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); + display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); + display->setColor(WHITE); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); + display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); + display->setColor(WHITE); + + // Text + display->drawString(boxLeft + padding, boxTop + padding, buf); + } } // ============================= @@ -513,10 +671,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t // ============================= #ifndef USE_EINK -void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +// Node list for Last Heard and Hop Signal views +void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // Static variables to track mode and duration - static NodeListMode lastRenderedMode = MODE_COUNT; + static ListMode_Node lastRenderedMode = MODE_COUNT_NODE; static unsigned long modeStartTime = 0; unsigned long now = millis(); @@ -529,23 +688,65 @@ void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, } #endif // On very first call (on boot or state enter) - if (lastRenderedMode == MODE_COUNT) { - currentMode = MODE_LAST_HEARD; + if (lastRenderedMode == MODE_COUNT_NODE) { + currentMode_Nodes = MODE_LAST_HEARD; modeStartTime = now; } // Time to switch to next mode? if (now - modeStartTime >= getModeCycleIntervalMs()) { - currentMode = static_cast((currentMode + 1) % MODE_COUNT); + currentMode_Nodes = static_cast((currentMode_Nodes + 1) % MODE_COUNT_NODE); modeStartTime = now; } // Render screen based on currentMode - const char *title = getCurrentModeTitle(display->getWidth()); - drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); + const char *title = getCurrentModeTitle_Nodes(display->getWidth()); + drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes); // Track the last mode to avoid reinitializing modeStartTime - lastRenderedMode = currentMode; + lastRenderedMode = currentMode_Nodes; +} + +// Node list for Distance and Bearings views +void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Static variables to track mode and duration + static ListMode_Location lastRenderedMode = MODE_COUNT_LOCATION; + static unsigned long modeStartTime = 0; + + unsigned long now = millis(); + +#if defined(M5STACK_UNITC6L) + display->clear(); + if (now - lastSwitchTime >= 3000) { + display->display(); + lastSwitchTime = now; + } +#endif + // On very first call (on boot or state enter) + if (lastRenderedMode == MODE_COUNT_LOCATION) { + currentMode_Location = MODE_DISTANCE; + modeStartTime = now; + } + + // Time to switch to next mode? + if (now - modeStartTime >= getModeCycleIntervalMs()) { + currentMode_Location = static_cast((currentMode_Location + 1) % MODE_COUNT_LOCATION); + modeStartTime = now; + } + + // Render screen based on currentMode + const char *title = getCurrentModeTitle_Location(display->getWidth()); + + // Render screen based on currentMode_Location + if (currentMode_Location == MODE_DISTANCE) { + drawNodeListScreen(display, state, x, y, title, drawNodeDistance); + } else if (currentMode_Location == MODE_BEARING) { + drawNodeListWithCompasses(display, state, x, y); + } + + // Track the last mode to avoid reinitializing modeStartTime + lastRenderedMode = currentMode_Location; } #endif @@ -566,14 +767,12 @@ void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ #endif 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; diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index ea8df8bd9..e212c031b 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -23,8 +23,11 @@ namespace NodeListRenderer 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 list mode enumeration -enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 }; +// Node list mode enumeration for Last Heard and Hop Signal views +enum ListMode_Node { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_COUNT_NODE = 2 }; + +// Node list mode enumeration for Distance and Bearings views +enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATION = 2 }; // Main node list screen function void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, @@ -35,7 +38,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t 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 drawEntryDynamic_Nodes(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 @@ -46,14 +49,20 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 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 drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDynamicListScreen_Location(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); -const char *getSafeNodeName(meshtastic_NodeInfoLite *node); +const char *getCurrentModeTitle_Nodes(int screenWidth); +const char *getCurrentModeTitle_Location(int screenWidth); +const char *getSafeNodeName(meshtastic_NodeInfoLite *node, int columnWidth); void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); +// Scrolling controls +void scrollUp(); +void scrollDown(); + // Bitmap drawing function void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display); diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index e95cc1610..8d76b4592 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if HAS_SCREEN +#if HAS_SCREEN #include "DisplayFormatters.h" #include "NodeDB.h" #include "NotificationRenderer.h" @@ -38,7 +38,7 @@ extern bool hasUnreadMessage; namespace graphics { - +int bannerSignalBars = -1; InputEvent NotificationRenderer::inEvent; int8_t NotificationRenderer::curSelected = 0; char NotificationRenderer::alertBannerMessage[256] = {0}; @@ -321,7 +321,7 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta } if (i == curSelected) { selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { strncpy(scratchLineBuffer[scratchLineNum], "> ", 3); strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36); strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3); @@ -449,7 +449,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { if (i == curSelected) { - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { strncpy(lineBuffer, "> ", 3); strncpy(lineBuffer + 2, optionsArrayPtr[i], 36); strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3); @@ -477,7 +477,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay bool is_picker = false; uint16_t lineCount = 0; - // === Layout Configuration === + // Layout Configuration constexpr uint16_t hPadding = 5; constexpr uint16_t vPadding = 2; bool needs_bell = false; @@ -491,13 +491,32 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); + // Track widest line INCLUDING bars (but don't change per-line widths) + uint16_t widestLineWithBars = 0; + while (lines[lineCount] != nullptr) { auto newlinePointer = strchr(lines[lineCount], '\n'); if (newlinePointer) lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first else // if the newline wasn't found, then pull string length from strlen lineLengths[lineCount] = strlen(lines[lineCount]); + lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true); + + // Consider extra width for signal bars on lines that contain "Signal:" + uint16_t potentialWidth = lineWidths[lineCount]; + if (graphics::bannerSignalBars >= 0 && strncmp(lines[lineCount], "Signal:", 7) == 0) { + const int totalBars = 5; + const int barWidth = 3; + const int barSpacing = 2; + const int gap = 6; // space between text and bars + int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap; + potentialWidth += barsWidth; + } + + if (potentialWidth > widestLineWithBars) + widestLineWithBars = potentialWidth; + if (!is_picker) { needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr); if (lineWidths[lineCount] > maxWidth) @@ -507,12 +526,16 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay } // count lines + // Ensure box accounts for signal bars if present + if (widestLineWithBars > maxWidth) + maxWidth = widestLineWithBars; + uint16_t boxWidth = hPadding * 2 + maxWidth; -#if defined(M5STACK_UNITC6L) + if (needs_bell) { - if (isHighResolution && boxWidth <= 150) + if ((currentResolution == ScreenResolution::High) && boxWidth <= 150) boxWidth += 26; - if (!isHighResolution && boxWidth <= 100) + if ((currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) && boxWidth <= 100) boxWidth += 20; } @@ -521,14 +544,17 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay uint8_t visibleTotalLines = std::min(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight); uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; uint16_t boxHeight = contentHeight + vPadding * 2; - if (visibleTotalLines == 1) - boxHeight += (isHighResolution ? 4 : 3); + if (visibleTotalLines == 1) { + boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3; + } int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); - if (totalLines > visibleTotalLines) - boxWidth += (isHighResolution ? 4 : 2); + if (totalLines > visibleTotalLines) { + boxWidth += (currentResolution == ScreenResolution::High) ? 4 : 2; + } int16_t boxTop = (display->height() / 2) - (boxHeight / 2); - + boxHeight += (currentResolution == ScreenResolution::High) ? 2 : 1; +#if defined(M5STACK_UNITC6L) if (visibleTotalLines == 1) { boxTop += 25; } @@ -539,127 +565,9 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay if (boxTop < 0) boxTop = 0; } +#endif - // === Draw Box === - display->setColor(BLACK); - display->fillRect(boxLeft, boxTop, boxWidth, boxHeight); - display->setColor(WHITE); - display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); - display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); - display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); - display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); - display->setColor(BLACK); - display->fillRect(boxLeft, boxTop, 1, 1); - display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); - display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); - display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); - display->setColor(WHITE); - int16_t lineY = boxTop + vPadding; - int swingRange = 8; - static int swingOffset = 0; - static bool swingRight = true; - static unsigned long lastSwingTime = 0; - unsigned long now = millis(); - int swingSpeedMs = 10 / (swingRange * 2); - if (now - lastSwingTime >= (unsigned long)swingSpeedMs) { - lastSwingTime = now; - if (swingRight) { - swingOffset++; - if (swingOffset >= swingRange) - swingRight = false; - } else { - swingOffset--; - if (swingOffset <= 0) - swingRight = true; - } - } - for (int i = 0; i < lineCount; i++) { - bool isTitle = (i == 0); - int globalOptionIndex = (i - 1) + firstOptionToShow; - bool isSelectedOption = (!isTitle && globalOptionIndex >= 0 && globalOptionIndex == curSelected); - - uint16_t visibleWidth = 64 - hPadding * 2; - if (totalLines > visibleTotalLines) - visibleWidth -= 6; - char lineBuffer[lineLengths[i] + 1]; - strncpy(lineBuffer, lines[i], lineLengths[i]); - lineBuffer[lineLengths[i]] = '\0'; - - if (isTitle) { - if (visibleTotalLines == 1) { - display->setColor(BLACK); - display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight); - display->setColor(WHITE); - display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer); - } else { - display->setColor(WHITE); - display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight); - display->setColor(BLACK); - display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer); - display->setColor(WHITE); - if (needs_bell) { - int bellY = boxTop + (FONT_HEIGHT_SMALL - 8) / 2; - display->drawXbm(boxLeft + (boxWidth - lineWidths[i]) / 2 - 10, bellY, 8, 8, bell_alert); - display->drawXbm(boxLeft + (boxWidth + lineWidths[i]) / 2 + 2, bellY, 8, 8, bell_alert); - } - } - lineY = boxTop + effectiveLineHeight + 1; - } else if (isSelectedOption) { - display->setColor(WHITE); - display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight); - display->setColor(BLACK); - if (lineLengths[i] > 15 && lineWidths[i] > visibleWidth) { - int textX = boxLeft + hPadding + swingOffset; - display->drawString(textX, lineY - 1, lineBuffer); - } else { - display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY - 1, lineBuffer); - } - display->setColor(WHITE); - lineY += effectiveLineHeight; - } else { - display->setColor(BLACK); - display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight); - display->setColor(WHITE); - display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY, lineBuffer); - lineY += effectiveLineHeight; - } - } - if (totalLines > visibleTotalLines) { - const uint8_t scrollBarWidth = 5; - int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; - int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; - uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight; - float ratio = (float)visibleTotalLines / totalLines; - uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4); - float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines); - uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight); - display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); - display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); - } -#else - if (needs_bell) { - if (isHighResolution && boxWidth <= 150) - boxWidth += 26; - if (!isHighResolution && boxWidth <= 100) - boxWidth += 20; - } - - uint16_t screenHeight = display->height(); - uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; - uint8_t visibleTotalLines = std::min(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight); - uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; - uint16_t boxHeight = contentHeight + vPadding * 2; - if (visibleTotalLines == 1) { - boxHeight += (isHighResolution) ? 4 : 3; - } - - int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); - if (totalLines > visibleTotalLines) { - boxWidth += (isHighResolution) ? 4 : 2; - } - int16_t boxTop = (display->height() / 2) - (boxHeight / 2); - - // === Draw Box === + // Draw Box display->setColor(BLACK); display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); @@ -675,7 +583,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); display->setColor(WHITE); - // === Draw Content === + // Draw Content int16_t lineY = boxTop + vPadding; for (int i = 0; i < lineCount; i++) { int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; @@ -704,17 +612,47 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay lineY += (effectiveLineHeight - 2 - background_yOffset); } else { // Pop-up - display->drawString(textX, lineY, lineBuffer); + // If this is the Signal line, center text + bars as one group + bool isSignalLine = (graphics::bannerSignalBars >= 0 && strstr(lineBuffer, "Signal:") != nullptr); + if (isSignalLine) { + const int totalBars = 5; + const int barWidth = 3; + const int barSpacing = 2; + const int barHeightStep = 2; + const int gap = 6; + + int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true); + int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap; + int totalWidth = textWidth + barsWidth; + int groupStartX = boxLeft + (boxWidth - totalWidth) / 2; + + display->drawString(groupStartX, lineY, lineBuffer); + + int baseX = groupStartX + textWidth + gap; + int baseY = lineY + effectiveLineHeight - 1; + for (int b = 0; b < totalBars; b++) { + int barHeight = (b + 1) * barHeightStep; + int x = baseX + b * (barWidth + barSpacing); + int y = baseY - barHeight; + + if (b < graphics::bannerSignalBars) { + display->fillRect(x, y, barWidth, barHeight); + } else { + display->drawRect(x, y, barWidth, barHeight); + } + } + } else { + display->drawString(textX, lineY, lineBuffer); + } lineY += (effectiveLineHeight); } } - // === Scroll Bar (Thicker, inside box, not over title) === + // Scroll Bar (Thicker, inside box, not over title) if (totalLines > visibleTotalLines) { const uint8_t scrollBarWidth = 5; - int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; - int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; // start after title line + int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight; float ratio = (float)visibleTotalLines / totalLines; @@ -725,7 +663,6 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); } -#endif } /// Draw the last text message we received diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 1f01640bf..7ce9d5afe 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -6,10 +6,7 @@ #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/TimeFormatters.h" #include "graphics/images.h" @@ -29,6 +26,16 @@ namespace graphics NodeNum UIRenderer::currentFavoriteNodeNum = 0; std::vector graphics::UIRenderer::favoritedNodes; +static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y) +{ + int yOffset = (currentResolution == ScreenResolution::High) ? -5 : 1; + if (currentResolution == ScreenResolution::High) { + NodeListRenderer::drawScaledXBitmap16x16(x, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite, display); + } else { + display->drawXbm(x + 1, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite); + } +} + void graphics::UIRenderer::rebuildFavoritedNodes() { favoritedNodes.clear(); @@ -56,7 +63,7 @@ extern uint32_t dopThresholds[5]; void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { // Draw satellite image - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display); } else { display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite); @@ -76,7 +83,7 @@ void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const mesht } else { snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites()); } - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { display->drawString(x + 18, y, textString); } else { display->drawString(x + 11, y, textString); @@ -244,16 +251,16 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, // 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) + bool show_total, const char *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()); + snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words); 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()); + snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words); } #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ @@ -261,19 +268,19 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); } else { display->drawFastImage(x, y + 3, 8, 8, imgUser); } #else - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); } else { display->drawFastImage(x, y + 1, 8, 8, imgUser); } #endif - int string_offset = (isHighResolution) ? 9 : 0; + int string_offset = (currentResolution == ScreenResolution::High) ? 9 : 0; display->drawString(x + 10 + string_offset, y - 2, usersString); } @@ -321,11 +328,12 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st int line = 1; // which slot to use next std::string usernameStr; // === 1. Long Name (always try to show first) === -#if defined(M5STACK_UNITC6L) - const char *username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr; -#else - const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; -#endif + const char *username; + if (currentResolution == ScreenResolution::UltraLow) { + username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr; + } else { + username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; + } if (username) { usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case @@ -501,7 +509,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st const int margin = 4; // --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- #if defined(USE_EINK) - const int iconSize = (isHighResolution) ? 16 : 8; + const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; const int navBarHeight = iconSize + 6; #else const int navBarHeight = 0; @@ -559,11 +567,11 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); // === Header === -#if defined(M5STACK_UNITC6L) - graphics::drawCommonHeader(display, x, y, "Home"); -#else - graphics::drawCommonHeader(display, x, y, ""); -#endif + if (currentResolution == ScreenResolution::UltraLow) { + graphics::drawCommonHeader(display, x, y, "Home"); + } else { + graphics::drawCommonHeader(display, x, y, ""); + } // === Content below header === @@ -578,15 +586,15 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta config.display.heading_bold = false; // Display Region and Channel Utilization -#if defined(M5STACK_UNITC6L) - drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); -#else - drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); -#endif + if (currentResolution == ScreenResolution::UltraLow) { + drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); + } else { + drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); + } char uptimeStr[32] = ""; -#if !defined(M5STACK_UNITC6L) - getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); -#endif + if (currentResolution != ScreenResolution::UltraLow) { + getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); + } display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr); // === Second Row: Satellites and Voltage === @@ -600,15 +608,8 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - int yOffset = (isHighResolution) ? 3 : 1; - if (isHighResolution) { - 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 = (isHighResolution) ? 6 : 0; + drawSatelliteIcon(display, x, getTextPositions(display)[line]); + int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine); } else { UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus); @@ -647,21 +648,22 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta char chUtilPercentage[10]; snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); - int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; + int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 + : display->getStringWidth(chUtil) + 5; int chUtil_y = getTextPositions(display)[line] + 3; - int chutil_bar_width = (isHighResolution) ? 100 : 50; + int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; if (!config.bluetooth.enabled) { #if defined(USE_EINK) - chutil_bar_width = (isHighResolution) ? 50 : 30; + chutil_bar_width = (currentResolution == ScreenResolution::High) ? 50 : 30; #else - chutil_bar_width = (isHighResolution) ? 80 : 40; + chutil_bar_width = (currentResolution == ScreenResolution::High) ? 80 : 40; #endif } - int chutil_bar_height = (isHighResolution) ? 12 : 7; - int extraoffset = (isHighResolution) ? 6 : 3; + int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; + int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; if (!config.bluetooth.enabled) { - extraoffset = (isHighResolution) ? 6 : 1; + extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 1; } int chutil_percent = airTime->channelUtilizationPercent(); @@ -721,7 +723,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta // === Fourth & Fifth Rows: Node Identity === int textWidth = 0; int nameX = 0; - int yOffset = (isHighResolution) ? 0 : 5; + int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5; std::string longNameStr; if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { @@ -759,7 +761,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta // Start Functions to write date/time to the screen // Helper function to check if a year is a leap year -bool isLeapYear(int year) +constexpr bool isLeapYear(int year) { return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); } @@ -990,15 +992,8 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - int yOffset = (isHighResolution) ? 3 : 1; - if (isHighResolution) { - 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 = (isHighResolution) ? 6 : 0; + drawSatelliteIcon(display, x, getTextPositions(display)[line]); + int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); } else { // Onboard GPS @@ -1156,7 +1151,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU 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; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { 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); @@ -1181,7 +1176,7 @@ void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, O display->setTextAlignment(TEXT_ALIGN_LEFT); const char *title = USERPREFS_OEM_TEXT; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); } display->setFont(FONT_SMALL); @@ -1225,15 +1220,15 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta lastFrameChangeTime = millis(); } - const int iconSize = isHighResolution ? 16 : 8; - const int spacing = isHighResolution ? 8 : 4; - const int bigOffset = isHighResolution ? 1 : 0; + const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; + const int spacing = (currentResolution == ScreenResolution::High) ? 8 : 4; + const int bigOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; const size_t totalIcons = screen->indicatorIcons.size(); if (totalIcons == 0) return; - const int navPadding = isHighResolution ? 24 : 12; // padding per side + const int navPadding = (currentResolution == ScreenResolution::High) ? 24 : 12; // padding per side int usableWidth = SCREEN_WIDTH - (navPadding * 2); if (usableWidth < iconSize) @@ -1300,7 +1295,7 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta display->setColor(BLACK); } - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display); } else { display->drawXbm(x, y, iconSize, iconSize, icon); @@ -1315,7 +1310,7 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta auto drawArrow = [&](bool rightSide) { display->setColor(WHITE); - const int offset = isHighResolution ? 3 : 1; + const int offset = (currentResolution == ScreenResolution::High) ? 3 : 1; const int halfH = rectHeight / 2; const int top = (y - 2) + (rectHeight - halfH) / 2; diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index 438d56cc2..6e37b68f2 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -34,7 +34,7 @@ class UIRenderer public: // Common UI elements 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 = ""); + int node_offset = 0, bool show_total = true, const char *additional_words = ""); // GPS status functions static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); @@ -43,9 +43,6 @@ class UIRenderer 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); @@ -83,8 +80,6 @@ class UIRenderer 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 diff --git a/src/graphics/emotes.cpp b/src/graphics/emotes.cpp index bed2b7b7c..aa54ef2f1 100644 --- a/src/graphics/emotes.cpp +++ b/src/graphics/emotes.cpp @@ -13,41 +13,81 @@ const Emote emotes[] = { {"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // 👎 Thumbs Down // --- Smileys (Multiple Unicode Aliases) --- - {"\U0001F60A", Smiling_Eyes, Smiling_Eyes_width, Smiling_Eyes_height}, // 😊 Smiling Eyes - {"\U0001F600", Grinning, Grinning_width, Grinning_height}, // 😀 Grinning Face - {"\U0001F642", Slightly_Smiling, Slightly_Smiling_width, Slightly_Smiling_height}, // 🙂 Slightly Smiling Face - {"\U0001F609", Winking_Face, Winking_Face_width, Winking_Face_height}, // 😉 Winking Face - {"\U0001F601", Grinning_Smiling_Eyes, Grinning_Smiling_Eyes_width, Grinning_Smiling_Eyes_height}, // 😁 Grinning Smiling Eyes - {"\U0001F60D", Heart_eyes, Heart_eyes_width, Heart_eyes_height}, // 😍 Heart Eyes + {"\U0001F60A", smiling_eyes, smiling_eyes_width, smiling_eyes_height}, // 😊 Smiling Eyes + {"\U0001F600", grinning, grinning_width, grinning_height}, // 😀 Grinning Face + {"\U0001F642", slightly_smiling, slightly_smiling_width, slightly_smiling_height}, // 🙂 Slightly Smiling Face + {"\U0001F609", winking_face, winking_face_width, winking_face_height}, // 😉 Winking Face + {"\U0001F601", grinning_smiling_eyes, grinning_smiling_eyes_width, grinning_smiling_eyes_height}, // 😁 Grinning Smiling Eyes + {"\U0001F60D", heart_eyes, heart_eyes_width, heart_eyes_height}, // 😍 Heart Eyes {"\U0001F970", heart_smile, heart_smile_width, heart_smile_height}, // 🥰 Smiling Face with Hearts // --- Question/Alert --- - {"\u2753", question, question_width, question_height}, // ❓ Question Mark - {"\u203C\uFE0F", bang, bang_width, bang_height}, // ‼️ Double Exclamation Mark + {"\u2753", question, question_width, question_height}, // ❓ Question Mark + {"\u203C\uFE0F", bang, bang_width, bang_height}, // ‼️ Double Exclamation Mark + {"\u26A0\uFE0F", caution, caution_width, caution_height}, // ⚠️ Warning Sign // --- Laughing Faces --- {"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy - {"\U0001F923", ROFL, ROFL_width, ROFL_height}, // 🤣 Rolling on the Floor Laughing - {"\U0001F606", Smiling_Closed_Eyes, Smiling_Closed_Eyes_width, Smiling_Closed_Eyes_height}, // 😆 Smiling Closed Eyes + {"\U0001F923", rofl, rofl_width, rofl_height}, // 🤣 Rolling on the Floor Laughing + {"\U0001F606", smiling_closed_eyes, smiling_closed_eyes_width, smiling_closed_eyes_height}, // 😆 Smiling Closed Eyes {"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat - {"\U0001F604", Grinning_SmilingEyes2, Grinning_SmilingEyes2_width, - Grinning_SmilingEyes2_height}, // 😄 Grinning Face with Smiling Eyes - {"\U0001F62D", Loudly_Crying_Face, Loudly_Crying_Face_width, Loudly_Crying_Face_height}, // 😭 Loudly Crying Face + {"\U0001F604", grinning_smiling_eyes_2, grinning_smiling_eyes_2_width, + grinning_smiling_eyes_2_height}, // 😄 Grinning Face with Smiling Eyes + {"\U0001F62D", loudly_crying_face, loudly_crying_face_width, loudly_crying_face_height}, // 😭 Loudly Crying Face + {"\U0001F92E", vomiting, vomiting_width, vomiting_height}, // 🤮 Face Vomiting + {"\U0001F60E", cool, cool_width, cool_height}, // 😎 Smiling Face with Sunglasses + {"\U0001F440", eyes, eyes_width, eyes_height}, // 👀 Eyes + {"\U0001F441\uFE0F", eye, eye_width, eye_height}, // 👁️ Eye // --- Gestures and People --- {"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // 👋 Waving Hand {"\u270C\uFE0F", peace_sign, peace_sign_width, peace_sign_height}, // ✌️ Victory Hand {"\U0001F596", vulcan_salute, vulcan_salute_width, vulcan_salute_height}, // 🖖 Vulcan Salute - {"\U0001F64F", Praying, Praying_width, Praying_height}, // 🙏 Praying Hands + {"\U0001F64F", praying, praying_width, praying_height}, // 🙏 Praying Hands + {"\U0001F4AA", strong, strong_width, strong_height}, // 💪 Flexed Biceps + {"\U0001F937", shrug, shrug_width, shrug_height}, // 🤷 Person Shrugging {"\U0001F920", cowboy, cowboy_width, cowboy_height}, // 🤠 Cowboy Hat Face {"\U0001F3A7", deadmau5, deadmau5_width, deadmau5_height}, // 🎧 Headphones + // --- Symbols --- + {"\u2714\uFE0F", check_mark, check_mark_width, check_mark_height}, // ✔️ Check Mark + {"\u2705", check_mark, check_mark_width, check_mark_height}, // ✅ Check Mark Button + {"\u2611\uFE0F", check_mark, check_mark_width, check_mark_height}, // ☑️ Check Box with Check + {"\U0001F3E0", house, house_width, house_height}, // 🏠 House + // --- 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 + {"\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 + {"\u2744\uFE0F", snowflake, snowflake_width, snowflake_height}, // ❄️ Snowflake + {"\U0001F4A7", drop, drop_width, drop_height}, // 💧 Droplet + {"\U0001F321\uFE0F", thermometer, thermometer_width, thermometer_height}, // 🌡️ Thermometer + {"\U0001F326\uFE0F", sun_behind_raincloud, sun_behind_raincloud_width, + sun_behind_raincloud_height}, // 🌦️ Sun Behind Rain Cloud + {"\u26C5", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅ Sun Behind Cloud + {"\u26C5\uFE0F", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅️ Sun Behind Cloud + {"\U0001F328\uFE0F", cloud_with_snow, cloud_with_snow_width, cloud_with_snow_height}, // 🌨️ Cloud with Snow + {"\U0001F329\uFE0F", cloud_with_lightning, cloud_with_lightning_width, + cloud_with_lightning_height}, // 🌩️ Cloud with Lightning + {"\u26C8", cloud_with_lightning_rain, cloud_with_lightning_rain_width, + cloud_with_lightning_rain_height}, // ⛈ Cloud with Lightning and Rain + {"\u26C8\uFE0F", cloud_with_lightning_rain, cloud_with_lightning_rain_width, + cloud_with_lightning_rain_height}, // ⛈️ Cloud with Lightning and Rain + {"\U0001F32C\uFE0F", wind_face, wind_face_width, wind_face_height}, // 🌬️ Wind Face + + // --- Moon Phases --- + {"\U0001F311", new_moon, new_moon_width, new_moon_height}, // 🌑 New Moon + {"\U0001F312", waxing_crescent_moon, waxing_crescent_moon_width, waxing_crescent_moon_height}, // 🌒 Waxing Crescent Moon + {"\U0001F313", first_quarter_moon, first_quarter_moon_width, first_quarter_moon_height}, // 🌓 First Quarter Moon + {"\U0001F314", waxing_gibbous_moon, waxing_gibbous_moon_width, waxing_gibbous_moon_height}, // 🌔 Waxing Gibbous Moon + {"\U0001F315", full_moon, full_moon_width, full_moon_height}, // 🌕 Full Moon + {"\U0001F316", waning_gibbous_moon, waning_gibbous_moon_width, waning_gibbous_moon_height}, // 🌖 Waning Gibbous Moon + {"\U0001F317", last_quarter_moon, last_quarter_moon_width, last_quarter_moon_height}, // 🌗 Last Quarter Moon + {"\U0001F318", waning_crescent_moon, waning_crescent_moon_width, waning_crescent_moon_height}, // 🌘 Waning Crescent Moon + {"\U0001F31B", first_quarter_moon_face, first_quarter_moon_face_width, + first_quarter_moon_face_height}, // 🌛 First Quarter Moon Face // --- Misc Faces --- {"\U0001F608", devil, devil_width, devil_height}, // 😈 Smiling Face with Horns @@ -67,13 +107,49 @@ const Emote emotes[] = { {"\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 - {"\U0001F36A", cookie, cookie_width, cookie_height}, // 🍪 Cookie - {"\U0001F525", Fire, Fire_width, Fire_height}, // 🔥 Fire - {"\u2728", Sparkles, Sparkles_width, Sparkles_height}, // ✨ Sparkles - {"\U0001F573\uFE0F", hole, hole_width, hole_height}, // 🕳️ Hole - {"\U0001F3B3", bowling, bowling_width, bowling_height} // 🎳 Bowling + {"\U0001F4A9", poo, poo_width, poo_height}, // 💩 Pile of Poo + {"\U0001F514", bell_icon, bell_icon_width, bell_icon_height}, // 🔔 Bell + {"\U0001F4CB", clipboard, clipboard_width, clipboard_height}, // 📋 Clipboard + {"\U0001F36A", cookie, cookie_width, cookie_height}, // 🍪 Cookie + {"\U0001F370", shortcake, shortcake_width, shortcake_height}, // 🍰 Shortcake + {"\U0001F351", peach, peach_width, peach_height}, // 🍑 Peach + {"\U0001F983", turkey, turkey_width, turkey_height}, // 🦃 Turkey + {"\U0001F357", turkey_leg, turkey_leg_width, turkey_leg_height}, // 🍗 Poultry Leg + {"\U0001F525", fire, fire_width, fire_height}, // 🔥 Fire + {"\u2728", sparkles, sparkles_width, sparkles_height}, // ✨ Sparkles + {"\U0001F573\uFE0F", hole, hole_width, hole_height}, // 🕳️ Hole + {"\U0001F3B3", bowling, bowling_width, bowling_height}, // 🎳 Bowling + + // --- Arrows --- + {"\u2193", downwards_arrow, downwards_arrow_width, downwards_arrow_height}, // ↓ Downwards Arrow + {"\u2193\uFE0E", downwards_arrow, downwards_arrow_width, downwards_arrow_height}, // ↓︎ Downwards Arrow (text) + {"\u2193\uFE0F", downwards_arrow, downwards_arrow_width, downwards_arrow_height}, // ↓️ Downwards Arrow (emoji) + {"\u2199", south_west_arrow, south_west_arrow_width, south_west_arrow_height}, // ↙ South West Arrow + {"\u2199\uFE0E", south_west_arrow, south_west_arrow_width, south_west_arrow_height}, // ↙︎ South West Arrow (text) + {"\u2199\uFE0F", south_west_arrow, south_west_arrow_width, south_west_arrow_height}, // ↙️ South West Arrow (emoji) + {"\u2190", leftwards_arrow, leftwards_arrow_width, leftwards_arrow_height}, // ← Leftwards Arrow + {"\u2190\uFE0E", leftwards_arrow, leftwards_arrow_width, leftwards_arrow_height}, // ←︎ Leftwards Arrow (text) + {"\u2190\uFE0F", leftwards_arrow, leftwards_arrow_width, leftwards_arrow_height}, // ←️ Leftwards Arrow (emoji) + {"\u2196", north_west_arrow, north_west_arrow_width, north_west_arrow_height}, // ↖ North West Arrow + {"\u2196\uFE0E", north_west_arrow, north_west_arrow_width, north_west_arrow_height}, // ↖︎ North West Arrow (text) + {"\u2196\uFE0F", north_west_arrow, north_west_arrow_width, north_west_arrow_height}, // ↖️ North West Arrow (emoji) + {"\u2191", upwards_arrow, upwards_arrow_width, upwards_arrow_height}, // ↑ Upwards Arrow + {"\u2191\uFE0E", upwards_arrow, upwards_arrow_width, upwards_arrow_height}, // ↑︎ Upwards Arrow (text) + {"\u2191\uFE0F", upwards_arrow, upwards_arrow_width, upwards_arrow_height}, // ↑️ Upwards Arrow (emoji) + {"\u2197", north_east_arrow, north_east_arrow_width, north_east_arrow_height}, // ↗ North East Arrow + {"\u2197\uFE0E", north_east_arrow, north_east_arrow_width, north_east_arrow_height}, // ↗︎ North East Arrow (text) + {"\u2197\uFE0F", north_east_arrow, north_east_arrow_width, north_east_arrow_height}, // ↗️ North East Arrow (emoji) + {"\u2192", rightwards_arrow, rightwards_arrow_width, rightwards_arrow_height}, // → Rightwards Arrow + {"\u2192\uFE0E", rightwards_arrow, rightwards_arrow_width, rightwards_arrow_height}, // →︎ Rightwards Arrow (text) + {"\u2192\uFE0F", rightwards_arrow, rightwards_arrow_width, rightwards_arrow_height}, // →️ Rightwards Arrow (emoji) + {"\u2198", south_east_arrow, south_east_arrow_width, south_east_arrow_height}, // ↘ South East Arrow + {"\u2198\uFE0E", south_east_arrow, south_east_arrow_width, south_east_arrow_height}, // ↘︎ South East Arrow (text) + {"\u2198\uFE0F", south_east_arrow, south_east_arrow_width, south_east_arrow_height}, // ↘️ South East Arrow (emoji) + + // --- Halloween --- + {"\U0001F383", jack_o_lantern, jack_o_lantern_width, jack_o_lantern_height}, // 🎃 Jack-O-Lantern + {"\U0001F47B", ghost, ghost_width, ghost_height}, // 👻 Ghost + {"\U0001F480", skull, skull_width, skull_height} // 💀 Skull #endif }; @@ -88,23 +164,23 @@ const unsigned char thumbdown[] PROGMEM = {0xF0, 0x1F, 0x08, 0x20, 0x06, 0x30, 0 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x3F, 0x18, 0x02, 0x20, 0x02, 0x40, 0x04, 0x80, 0x04, 0x80, 0x04, 0x00, 0x03, 0x00, 0x00}; -const unsigned char Smiling_Eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, +const unsigned char smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x02, 0x40, 0x02, 0x40, 0x22, 0x44, 0x22, 0x44, 0xC2, 0x43, 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, +const unsigned char grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, 0x42, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Slightly_Smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, +const unsigned char slightly_smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, 0x42, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Winking_Face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42, +const unsigned char winking_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42, 0x46, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Grinning_Smiling_Eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, +const unsigned char grinning_smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x02, 0x40, 0xFA, 0x5F, 0x0A, 0x50, 0x0A, 0x50, 0x12, 0x48, 0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; @@ -112,7 +188,7 @@ const unsigned char heart_smile[] PROGMEM = {0x00, 0x00, 0x6C, 0x07, 0x7C, 0x18, 0x0A, 0x02, 0xD8, 0x02, 0xF8, 0x22, 0xFC, 0x20, 0x74, 0xDB, 0x23, 0x1F, 0x00, 0x1F, 0x20, 0x0E, 0x18, 0xE4, 0x07, 0x00, 0x00}; -const unsigned char Heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA, +const unsigned char heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA, 0x5F, 0x72, 0x4E, 0x22, 0x44, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; @@ -128,19 +204,19 @@ const unsigned char haha[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x4A, 0x0A, 0x50, 0x0E, 0x70, 0xF2, 0x4F, 0x12, 0x48, 0x32, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char ROFL[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02, +const unsigned char rofl[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02, 0x4C, 0x02, 0x4A, 0x1A, 0x49, 0x8A, 0x48, 0x42, 0x48, 0x22, 0x44, 0xE4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Smiling_Closed_Eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42, +const unsigned char smiling_closed_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42, 0x42, 0x22, 0x44, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Grinning_SmilingEyes2[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, - 0x4A, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, - 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char grinning_smiling_eyes_2[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, + 0x4A, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, + 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Loudly_Crying_Face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A, +const unsigned char loudly_crying_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A, 0x52, 0x12, 0x48, 0x12, 0x48, 0x92, 0x49, 0x52, 0x4A, 0x52, 0x4A, 0x54, 0x2A, 0x94, 0x29, 0x18, 0x18, 0xF0, 0x0F, 0x00, 0x00}; @@ -192,7 +268,7 @@ const unsigned char cookie[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04 0x40, 0x02, 0x58, 0x82, 0x5B, 0x92, 0x43, 0x82, 0x43, 0x02, 0x40, 0x64, 0x28, 0x64, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, +const unsigned char fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, 0x1F, 0xFE, 0x3E, 0x7E, 0x3E, 0x3E, 0x7C, 0x1E, 0x78, 0x1E, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x38, 0x38, 0x30, 0x38, 0x60, 0x0C}; @@ -200,11 +276,11 @@ const unsigned char peace_sign[] PROGMEM = {0xC0, 0x30, 0x40, 0x29, 0x40, 0x25, 0x0A, 0x54, 0x68, 0x54, 0x58, 0x54, 0x44, 0x3C, 0x22, 0x04, 0x22, 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90, +const unsigned char praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90, 0x09, 0x98, 0x19, 0x94, 0x29, 0xA4, 0x25, 0xA4, 0x25, 0x84, 0x21, 0x84, 0x21, 0x86, 0x61, 0x4E, 0x72, 0x7F, 0x7E, 0x3F, 0xFC}; -const unsigned char Sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00, +const unsigned char sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00, 0x1F, 0x80, 0x3F, 0xE0, 0xFF, 0x80, 0x3F, 0x10, 0x1F, 0x10, 0x0E, 0x38, 0x04, 0xFE, 0x04, 0x38, 0x00, 0x10, 0x00, 0x10, 0x00}; @@ -227,7 +303,179 @@ const unsigned char bowling[] PROGMEM = {0x00, 0x38, 0x00, 0x44, 0x00, 0x44, 0x0 const unsigned char vulcan_salute[] PROGMEM = {0x08, 0x02, 0x16, 0x0D, 0x15, 0x15, 0x15, 0x15, 0xA9, 0x12, 0x4A, 0x0A, 0x02, 0x38, 0x04, 0x48, 0x04, 0x44, 0x04, 0x22, 0x04, 0x22, 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char jack_o_lantern[] PROGMEM = {0xC0, 0x00, 0x80, 0x01, 0xB8, 0x1D, 0xC4, 0x23, 0x22, 0x44, 0x05, + 0xA0, 0x31, 0x8C, 0x51, 0x8A, 0x61, 0x86, 0x09, 0x90, 0xB9, 0x9D, + 0x49, 0x92, 0xB2, 0x4D, 0x42, 0x42, 0x04, 0x20, 0xF8, 0x1F}; + +const unsigned char ghost[] PROGMEM = {0xC0, 0x03, 0xF0, 0x0F, 0xF8, 0x1F, 0xDC, 0x3B, 0xBC, 0x3D, 0xDF, + 0xFB, 0xFF, 0xFF, 0x1F, 0xF8, 0x1E, 0x78, 0x1C, 0x38, 0x3C, 0x3C, + 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0x8C, 0x31}; + +const unsigned char skull[] PROGMEM = {0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xC7, + 0xE3, 0x87, 0xE1, 0x87, 0xE1, 0x8F, 0xF1, 0xFE, 0x7F, 0x7C, 0x3E, + 0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xB0, 0x0D}; + +const unsigned char vomiting[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x22, + 0x44, 0x42, 0x42, 0x22, 0x44, 0x02, 0x40, 0x02, 0x40, 0xC2, 0x43, + 0x64, 0x26, 0x64, 0x26, 0x68, 0x16, 0x50, 0x0A, 0xF8, 0x1F}; + +const unsigned char cool[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0xFC, 0x3F, 0xFA, + 0x5F, 0x72, 0x4E, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, + 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char shortcake[] PROGMEM = {0x00, 0x00, 0x00, 0x0F, 0x80, 0x3F, 0xE0, 0xFC, 0xE0, 0xE1, 0xF0, + 0xB8, 0x10, 0x87, 0xC8, 0x80, 0x3C, 0xE0, 0x06, 0x98, 0x02, 0xC7, + 0xE2, 0x30, 0x1A, 0x0E, 0xC6, 0x01, 0x32, 0x00, 0x0E, 0x00}; + +const unsigned char caution[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x06, 0x60, + 0x06, 0x70, 0x0E, 0x70, 0x0E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, + 0xFC, 0x3F, 0x7E, 0x7E, 0x7E, 0x7E, 0xFC, 0x3F, 0x00, 0x00}; + +const unsigned char clipboard[] PROGMEM = {0xC0, 0x03, 0x7E, 0x7E, 0xC2, 0x43, 0xFA, 0x5F, 0x0A, 0x5B, 0xFA, + 0x5F, 0x8A, 0x54, 0xFA, 0x5F, 0x4A, 0x58, 0xFA, 0x5F, 0x2A, 0x51, + 0xFA, 0x5F, 0x0A, 0x59, 0xFA, 0x5F, 0x02, 0x40, 0xFE, 0x7F}; + +const unsigned char snowflake[] PROGMEM = {0x00, 0x00, 0x40, 0x01, 0x88, 0x08, 0x8C, 0x18, 0xD0, 0x05, 0x60, + 0x03, 0x32, 0x26, 0x1C, 0x1C, 0x32, 0x26, 0x60, 0x03, 0xD0, 0x05, + 0x8C, 0x18, 0x88, 0x08, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char drop[] PROGMEM = {0x00, 0x00, 0x00, 0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xE0, + 0x0F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF8, 0x3F, + 0xF8, 0x3F, 0xF0, 0x1F, 0xE0, 0x0F, 0x80, 0x03, 0x00, 0x00}; + +const unsigned char thermometer[] PROGMEM = {0x00, 0x00, 0x0C, 0x00, 0x16, 0x00, 0x2E, 0x00, 0x5C, 0x00, 0xB8, + 0x00, 0x70, 0x01, 0xE0, 0x02, 0xC0, 0x05, 0x80, 0x3B, 0x00, 0x47, + 0x00, 0xBE, 0x00, 0x9E, 0x00, 0xBE, 0x00, 0x7C, 0x00, 0x38}; + +const unsigned char sun_behind_raincloud[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x0E, 0x38, 0x1F, 0xFC, 0x37, 0xEE, + 0x77, 0xDE, 0x7B, 0x3E, 0x7C, 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12, + 0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char sun_behind_cloud[] PROGMEM = {0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x04, 0x0E, 0x3C, 0x1B, 0xFC, + 0x3B, 0xFE, 0x7B, 0xFA, 0x7B, 0xF6, 0x7D, 0x0C, 0x3E, 0xF8, 0x1F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char cloud_with_snow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, + 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, 0x08, 0x02, + 0x40, 0x10, 0x00, 0x00, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char cloud_with_lightning[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, + 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x80, 0x01, + 0x80, 0x01, 0xC0, 0x07, 0x00, 0x03, 0x00, 0x03, 0x00, 0x01}; + +const unsigned char cloud_with_lightning_rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, + 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x90, 0x21, + 0x90, 0x21, 0xC8, 0x17, 0x08, 0x13, 0x00, 0x03, 0x00, 0x01}; + +const unsigned char wind_face[] PROGMEM = {0xFF, 0x00, 0x01, 0x01, 0x01, 0x01, 0xF9, 0x00, 0xF9, 0x01, 0xD9, + 0x01, 0x99, 0x01, 0xF9, 0x01, 0xF9, 0x33, 0xFD, 0x4B, 0xFD, 0x85, + 0xFD, 0x9A, 0xFD, 0x75, 0xFD, 0x09, 0xFD, 0x01, 0xFF, 0x00}; + +const unsigned char new_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x02, + 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, + 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char waxing_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3E, 0x04, 0x3C, 0x02, + 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, + 0x04, 0x3C, 0x04, 0x3E, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char first_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3F, 0x04, 0x3F, 0x02, + 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, + 0x04, 0x3F, 0x04, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char waxing_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x84, 0x3F, 0xC4, 0x3F, 0xC2, + 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, + 0xC4, 0x3F, 0x84, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char full_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, + 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, + 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char waning_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x21, 0xFC, 0x23, 0xFE, + 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, + 0xFC, 0x23, 0xFC, 0x21, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char last_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x20, 0xFC, 0x20, 0xFE, + 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, + 0xFC, 0x20, 0xFC, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char waning_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0x7C, 0x20, 0x3C, 0x20, 0x1E, + 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, + 0x3C, 0x20, 0x7C, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char first_quarter_moon_face[] PROGMEM = {0x00, 0x0F, 0x00, 0x12, 0x00, 0x24, 0x00, 0x44, 0x00, 0x48, 0x00, + 0x88, 0x00, 0x84, 0x80, 0x93, 0x80, 0x80, 0x03, 0x81, 0x8D, 0x80, + 0x71, 0x40, 0x82, 0x41, 0x02, 0x20, 0x0C, 0x18, 0xF0, 0x07}; + +const unsigned char peach[] PROGMEM = {0x70, 0x0F, 0x88, 0x10, 0x78, 0x1F, 0x88, 0x11, 0x04, 0x22, 0x02, + 0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x42, 0x02, 0x40, + 0x04, 0x20, 0x04, 0x20, 0x08, 0x10, 0x30, 0x0C, 0xC0, 0x03}; + +const unsigned char turkey[] PROGMEM = {0x00, 0x00, 0x38, 0x00, 0x44, 0x38, 0x56, 0x54, 0x45, 0x52, 0xE2, + 0x21, 0x2C, 0x56, 0x14, 0x58, 0x0A, 0x37, 0x86, 0x68, 0x82, 0x50, + 0x82, 0x20, 0x04, 0x41, 0xF8, 0x7F, 0x40, 0x02, 0xF0, 0x07}; + +const unsigned char turkey_leg[] PROGMEM = {0x0C, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x2F, 0x00, 0x46, 0x00, 0x88, + 0x01, 0x10, 0x0E, 0x20, 0x30, 0x20, 0x40, 0x40, 0x40, 0x40, 0x80, + 0x40, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x43, 0x00, 0x3C}; + +const unsigned char south_west_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1C, 0x00, 0x3E, 0x00, + 0x1F, 0x80, 0x0F, 0xC2, 0x07, 0xE6, 0x03, 0xFE, 0x01, 0xFE, 0x00, + 0x7E, 0x00, 0x7E, 0x00, 0xFE, 0x00, 0xFE, 0x01, 0x00, 0x00}; + +const unsigned char south_east_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x38, 0x00, 0x7C, 0x00, 0xF8, + 0x00, 0xF0, 0x01, 0xE0, 0x43, 0xC0, 0x67, 0x80, 0x7F, 0x00, 0x7F, + 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0x00, 0x00}; + +const unsigned char north_west_arrow[] PROGMEM = {0x00, 0x00, 0xFE, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0xFE, + 0x00, 0xFE, 0x01, 0xE6, 0x03, 0xC2, 0x07, 0x80, 0x0F, 0x00, 0x1F, + 0x00, 0x3E, 0x00, 0x1C, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char north_east_arrow[] PROGMEM = {0x00, 0x00, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00, + 0x7F, 0x80, 0x7F, 0xC0, 0x67, 0xE0, 0x43, 0xF0, 0x01, 0xF8, 0x00, + 0x7C, 0x00, 0x38, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char downwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, + 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xFC, 0x3F, + 0xF8, 0x1F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01}; + +const unsigned char leftwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x30, 0x00, 0x38, 0x00, 0x3C, + 0x00, 0xFE, 0x3F, 0xFF, 0x3F, 0xFF, 0x3F, 0xFE, 0x3F, 0x3C, 0x00, + 0x38, 0x00, 0x30, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char upwards_arrow[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xFC, + 0x3F, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, + 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char rightwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0C, 0x00, 0x1C, 0x00, + 0x3C, 0xFC, 0x7F, 0xFC, 0xFF, 0xFC, 0xFF, 0xFC, 0x7F, 0x00, 0x3C, + 0x00, 0x1C, 0x00, 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char strong[] PROGMEM = {0x38, 0x00, 0x44, 0x00, 0x62, 0x00, 0x42, 0x00, 0x42, 0x00, 0x3A, + 0x00, 0x11, 0x3C, 0x11, 0x42, 0xD1, 0x81, 0x31, 0x82, 0x11, 0x82, + 0x21, 0x80, 0x01, 0x80, 0x01, 0x80, 0x02, 0x40, 0xFC, 0x3F}; + +const unsigned char check_mark[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x70, 0x00, 0x3C, 0x00, + 0x1E, 0x00, 0x0F, 0x80, 0x07, 0xC3, 0x03, 0xEE, 0x03, 0xFC, 0x01, + 0xF8, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x60, 0x00, 0x20, 0x00}; + +const unsigned char house[] PROGMEM = {0x80, 0x01, 0x5C, 0x02, 0x34, 0x04, 0x14, 0x08, 0x0C, 0x10, 0x04, + 0x20, 0x02, 0x40, 0xFF, 0xFF, 0x02, 0x40, 0x7A, 0x5F, 0x4A, 0x55, + 0x4A, 0x5F, 0x6A, 0x55, 0x4A, 0x5F, 0x4A, 0x40, 0xFE, 0x7F}; + +const unsigned char shrug[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x08, 0x50, 0x0A, 0x10, 0x08, 0x90, + 0x09, 0x27, 0xE4, 0x49, 0x92, 0xAA, 0x55, 0x16, 0x68, 0x12, 0x48, + 0x02, 0x40, 0x02, 0x40, 0x0C, 0x30, 0x08, 0x10, 0xF8, 0x1F}; + +const unsigned char eyes[] PROGMEM = {0x00, 0x00, 0x3C, 0x3C, 0x42, 0x42, 0x81, 0x81, 0x85, 0x85, 0x8F, + 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, + 0x85, 0x85, 0x81, 0x81, 0x42, 0x42, 0x3C, 0x3C, 0x00, 0x00}; + +const unsigned char eye[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xF4, + 0x2F, 0x7A, 0x5E, 0x39, 0x9C, 0x39, 0x9C, 0x7A, 0x5E, 0xF4, 0x2F, + 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; #endif } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/emotes.h b/src/graphics/emotes.h index b1b2d16da..0637712cc 100644 --- a/src/graphics/emotes.h +++ b/src/graphics/emotes.h @@ -22,33 +22,33 @@ extern const int numEmotes; extern const unsigned char thumbup[] PROGMEM; extern const unsigned char thumbdown[] PROGMEM; -#define Smiling_Eyes_height 16 -#define Smiling_Eyes_width 16 -extern const unsigned char Smiling_Eyes[] PROGMEM; +#define smiling_eyes_height 16 +#define smiling_eyes_width 16 +extern const unsigned char smiling_eyes[] PROGMEM; -#define Grinning_height 16 -#define Grinning_width 16 -extern const unsigned char Grinning[] PROGMEM; +#define grinning_height 16 +#define grinning_width 16 +extern const unsigned char grinning[] PROGMEM; -#define Slightly_Smiling_height 16 -#define Slightly_Smiling_width 16 -extern const unsigned char Slightly_Smiling[] PROGMEM; +#define slightly_smiling_height 16 +#define slightly_smiling_width 16 +extern const unsigned char slightly_smiling[] PROGMEM; -#define Winking_Face_height 16 -#define Winking_Face_width 16 -extern const unsigned char Winking_Face[] PROGMEM; +#define winking_face_height 16 +#define winking_face_width 16 +extern const unsigned char winking_face[] PROGMEM; -#define Grinning_Smiling_Eyes_height 16 -#define Grinning_Smiling_Eyes_width 16 -extern const unsigned char Grinning_Smiling_Eyes[] PROGMEM; +#define grinning_smiling_eyes_height 16 +#define grinning_smiling_eyes_width 16 +extern const unsigned char grinning_smiling_eyes[] PROGMEM; #define heart_smile_height 16 #define heart_smile_width 16 extern const unsigned char heart_smile[] PROGMEM; -#define Heart_eyes_height 16 -#define Heart_eyes_width 16 -extern const unsigned char Heart_eyes[] PROGMEM; +#define heart_eyes_height 16 +#define heart_eyes_width 16 +extern const unsigned char heart_eyes[] PROGMEM; #define question_height 16 #define question_width 16 @@ -62,21 +62,21 @@ extern const unsigned char bang[] PROGMEM; #define haha_width 16 extern const unsigned char haha[] PROGMEM; -#define ROFL_height 16 -#define ROFL_width 16 -extern const unsigned char ROFL[] PROGMEM; +#define rofl_height 16 +#define rofl_width 16 +extern const unsigned char rofl[] PROGMEM; -#define Smiling_Closed_Eyes_height 16 -#define Smiling_Closed_Eyes_width 16 -extern const unsigned char Smiling_Closed_Eyes[] PROGMEM; +#define smiling_closed_eyes_height 16 +#define smiling_closed_eyes_width 16 +extern const unsigned char smiling_closed_eyes[] PROGMEM; -#define Grinning_SmilingEyes2_height 16 -#define Grinning_SmilingEyes2_width 16 -extern const unsigned char Grinning_SmilingEyes2[] PROGMEM; +#define grinning_smiling_eyes_2_height 16 +#define grinning_smiling_eyes_2_width 16 +extern const unsigned char grinning_smiling_eyes_2[] PROGMEM; -#define Loudly_Crying_Face_height 16 -#define Loudly_Crying_Face_width 16 -extern const unsigned char Loudly_Crying_Face[] PROGMEM; +#define loudly_crying_face_height 16 +#define loudly_crying_face_width 16 +extern const unsigned char loudly_crying_face[] PROGMEM; #define wave_icon_height 16 #define wave_icon_width 16 @@ -126,21 +126,21 @@ extern const unsigned char bell_icon[] PROGMEM; #define cookie_height 16 extern const unsigned char cookie[] PROGMEM; -#define Fire_width 16 -#define Fire_height 16 -extern const unsigned char Fire[] PROGMEM; +#define fire_width 16 +#define fire_height 16 +extern const unsigned char fire[] PROGMEM; #define peace_sign_width 16 #define peace_sign_height 16 extern const unsigned char peace_sign[] PROGMEM; -#define Praying_width 16 -#define Praying_height 16 -extern const unsigned char Praying[] PROGMEM; +#define praying_width 16 +#define praying_height 16 +extern const unsigned char praying[] PROGMEM; -#define Sparkles_width 16 -#define Sparkles_height 16 -extern const unsigned char Sparkles[] PROGMEM; +#define sparkles_width 16 +#define sparkles_height 16 +extern const unsigned char sparkles[] PROGMEM; #define clown_width 16 #define clown_height 16 @@ -161,6 +161,178 @@ extern const unsigned char bowling[] PROGMEM; #define vulcan_salute_width 16 #define vulcan_salute_height 16 extern const unsigned char vulcan_salute[] PROGMEM; + +#define jack_o_lantern_width 16 +#define jack_o_lantern_height 16 +extern const unsigned char jack_o_lantern[] PROGMEM; + +#define ghost_width 16 +#define ghost_height 16 +extern const unsigned char ghost[] PROGMEM; + +#define skull_width 16 +#define skull_height 16 +extern const unsigned char skull[] PROGMEM; + +#define vomiting_width 16 +#define vomiting_height 16 +extern const unsigned char vomiting[] PROGMEM; + +#define cool_width 16 +#define cool_height 16 +extern const unsigned char cool[] PROGMEM; + +#define shortcake_width 16 +#define shortcake_height 16 +extern const unsigned char shortcake[] PROGMEM; + +#define caution_width 16 +#define caution_height 16 +extern const unsigned char caution[] PROGMEM; + +#define clipboard_width 16 +#define clipboard_height 16 +extern const unsigned char clipboard[] PROGMEM; + +#define snowflake_width 16 +#define snowflake_height 16 +extern const unsigned char snowflake[] PROGMEM; + +#define drop_width 16 +#define drop_height 16 +extern const unsigned char drop[] PROGMEM; + +#define thermometer_width 16 +#define thermometer_height 16 +extern const unsigned char thermometer[] PROGMEM; + +#define sun_behind_raincloud_width 16 +#define sun_behind_raincloud_height 16 +extern const unsigned char sun_behind_raincloud[] PROGMEM; + +#define sun_behind_cloud_width 16 +#define sun_behind_cloud_height 16 +extern const unsigned char sun_behind_cloud[] PROGMEM; + +#define cloud_with_snow_width 16 +#define cloud_with_snow_height 16 +extern const unsigned char cloud_with_snow[] PROGMEM; + +#define cloud_with_lightning_width 16 +#define cloud_with_lightning_height 16 +extern const unsigned char cloud_with_lightning[] PROGMEM; + +#define cloud_with_lightning_rain_width 16 +#define cloud_with_lightning_rain_height 16 +extern const unsigned char cloud_with_lightning_rain[] PROGMEM; + +#define wind_face_width 16 +#define wind_face_height 16 +extern const unsigned char wind_face[] PROGMEM; + +#define new_moon_width 16 +#define new_moon_height 16 +extern const unsigned char new_moon[] PROGMEM; + +#define waxing_crescent_moon_width 16 +#define waxing_crescent_moon_height 16 +extern const unsigned char waxing_crescent_moon[] PROGMEM; + +#define first_quarter_moon_width 16 +#define first_quarter_moon_height 16 +extern const unsigned char first_quarter_moon[] PROGMEM; + +#define waxing_gibbous_moon_width 16 +#define waxing_gibbous_moon_height 16 +extern const unsigned char waxing_gibbous_moon[] PROGMEM; + +#define full_moon_width 16 +#define full_moon_height 16 +extern const unsigned char full_moon[] PROGMEM; + +#define waning_gibbous_moon_width 16 +#define waning_gibbous_moon_height 16 +extern const unsigned char waning_gibbous_moon[] PROGMEM; + +#define last_quarter_moon_width 16 +#define last_quarter_moon_height 16 +extern const unsigned char last_quarter_moon[] PROGMEM; + +#define waning_crescent_moon_width 16 +#define waning_crescent_moon_height 16 +extern const unsigned char waning_crescent_moon[] PROGMEM; + +#define first_quarter_moon_face_width 16 +#define first_quarter_moon_face_height 16 +extern const unsigned char first_quarter_moon_face[] PROGMEM; + +#define peach_width 16 +#define peach_height 16 +extern const unsigned char peach[] PROGMEM; + +#define turkey_width 16 +#define turkey_height 16 +extern const unsigned char turkey[] PROGMEM; + +#define turkey_leg_width 16 +#define turkey_leg_height 16 +extern const unsigned char turkey_leg[] PROGMEM; + +#define south_west_arrow_width 16 +#define south_west_arrow_height 16 +extern const unsigned char south_west_arrow[] PROGMEM; + +#define south_east_arrow_width 16 +#define south_east_arrow_height 16 +extern const unsigned char south_east_arrow[] PROGMEM; + +#define north_west_arrow_width 16 +#define north_west_arrow_height 16 +extern const unsigned char north_west_arrow[] PROGMEM; + +#define north_east_arrow_width 16 +#define north_east_arrow_height 16 +extern const unsigned char north_east_arrow[] PROGMEM; + +#define downwards_arrow_width 16 +#define downwards_arrow_height 16 +extern const unsigned char downwards_arrow[] PROGMEM; + +#define leftwards_arrow_width 16 +#define leftwards_arrow_height 16 +extern const unsigned char leftwards_arrow[] PROGMEM; + +#define upwards_arrow_width 16 +#define upwards_arrow_height 16 +extern const unsigned char upwards_arrow[] PROGMEM; + +#define rightwards_arrow_width 16 +#define rightwards_arrow_height 16 +extern const unsigned char rightwards_arrow[] PROGMEM; + +#define strong_width 16 +#define strong_height 16 +extern const unsigned char strong[] PROGMEM; + +#define check_mark_width 16 +#define check_mark_height 16 +extern const unsigned char check_mark[] PROGMEM; + +#define house_width 16 +#define house_height 16 +extern const unsigned char house[] PROGMEM; + +#define shrug_width 16 +#define shrug_height 16 +extern const unsigned char shrug[] PROGMEM; + +#define eyes_width 16 +#define eyes_height 16 +extern const unsigned char eyes[] PROGMEM; + +#define eye_width 16 +#define eye_height 16 +extern const unsigned char eye[] PROGMEM; #endif // EXCLUDE_EMOJI -} // namespace graphics \ No newline at end of file +} // namespace graphics diff --git a/src/graphics/fonts/OLEDDisplayFontsGR.cpp b/src/graphics/fonts/OLEDDisplayFontsGR.cpp new file mode 100644 index 000000000..71f89ea6e --- /dev/null +++ b/src/graphics/fonts/OLEDDisplayFontsGR.cpp @@ -0,0 +1,429 @@ +#ifdef OLED_GR + +#include "OLEDDisplayFontsGR.h" + +/** + * Greek font for OLED displays - ArialMT Plain 10pt + * Contains ASCII 32-127 + Greek characters mapped to CP-1253 positions (192-254) + * + * Generated using ThingPulse OLED font converter + * Font: Arial, Size: 10px + * Character set: Basic Latin + Greek (Α-Ω, α-ω, accented) + * + * CP-1253 Greek character mapping: + * 193-209: Α Β Γ Δ Ε Ζ Η Θ Ι Κ Λ Μ Ν Ξ Ο Π Ρ + * 211-217: Σ Τ Υ Φ Χ Ψ Ω + * 225-241: α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ + * 242-249: ς σ τ υ φ χ ψ ω + */ +const uint8_t ArialMT_Plain_10_GR[] PROGMEM = { + 0x0A, // Width: 10 + 0x0D, // Height: 13 + 0x20, // First char: 32 + 0xE0, // Number of chars: 224 + + // Jump Table (4 bytes per character: offset high, offset low, size, width) + // Characters 32-127: Standard ASCII + 0xFF, 0xFF, 0x00, 0x03, // 32 space + 0x00, 0x00, 0x04, 0x03, // 33 ! + 0x00, 0x04, 0x05, 0x04, // 34 " + 0x00, 0x09, 0x09, 0x06, // 35 # + 0x00, 0x12, 0x0A, 0x06, // 36 $ + 0x00, 0x1C, 0x10, 0x09, // 37 % + 0x00, 0x2C, 0x0E, 0x08, // 38 & + 0x00, 0x3A, 0x01, 0x02, // 39 ' + 0x00, 0x3B, 0x06, 0x04, // 40 ( + 0x00, 0x41, 0x06, 0x04, // 41 ) + 0x00, 0x47, 0x05, 0x04, // 42 * + 0x00, 0x4C, 0x09, 0x06, // 43 + + 0x00, 0x55, 0x04, 0x03, // 44 , + 0x00, 0x59, 0x03, 0x03, // 45 - + 0x00, 0x5C, 0x04, 0x03, // 46 . + 0x00, 0x60, 0x05, 0x04, // 47 / + 0x00, 0x65, 0x0A, 0x06, // 48 0 + 0x00, 0x6F, 0x08, 0x05, // 49 1 + 0x00, 0x77, 0x0A, 0x06, // 50 2 + 0x00, 0x81, 0x0A, 0x06, // 51 3 + 0x00, 0x8B, 0x0B, 0x07, // 52 4 + 0x00, 0x96, 0x0A, 0x06, // 53 5 + 0x00, 0xA0, 0x0A, 0x06, // 54 6 + 0x00, 0xAA, 0x09, 0x06, // 55 7 + 0x00, 0xB3, 0x0A, 0x06, // 56 8 + 0x00, 0xBD, 0x0A, 0x06, // 57 9 + 0x00, 0xC7, 0x04, 0x03, // 58 : + 0x00, 0xCB, 0x04, 0x03, // 59 ; + 0x00, 0xCF, 0x0A, 0x06, // 60 < + 0x00, 0xD9, 0x09, 0x06, // 61 = + 0x00, 0xE2, 0x09, 0x06, // 62 > + 0x00, 0xEB, 0x0B, 0x07, // 63 ? + 0x00, 0xF6, 0x14, 0x0B, // 64 @ + 0x01, 0x0A, 0x0E, 0x08, // 65 A + 0x01, 0x18, 0x0C, 0x07, // 66 B + 0x01, 0x24, 0x0C, 0x07, // 67 C + 0x01, 0x30, 0x0B, 0x07, // 68 D + 0x01, 0x3B, 0x0C, 0x07, // 69 E + 0x01, 0x47, 0x09, 0x06, // 70 F + 0x01, 0x50, 0x0D, 0x08, // 71 G + 0x01, 0x5D, 0x0C, 0x07, // 72 H + 0x01, 0x69, 0x04, 0x03, // 73 I + 0x01, 0x6D, 0x08, 0x05, // 74 J + 0x01, 0x75, 0x0E, 0x08, // 75 K + 0x01, 0x83, 0x0C, 0x07, // 76 L + 0x01, 0x8F, 0x10, 0x09, // 77 M + 0x01, 0x9F, 0x0C, 0x07, // 78 N + 0x01, 0xAB, 0x0E, 0x08, // 79 O + 0x01, 0xB9, 0x0B, 0x07, // 80 P + 0x01, 0xC4, 0x0E, 0x08, // 81 Q + 0x01, 0xD2, 0x0C, 0x07, // 82 R + 0x01, 0xDE, 0x0C, 0x07, // 83 S + 0x01, 0xEA, 0x0B, 0x07, // 84 T + 0x01, 0xF5, 0x0C, 0x07, // 85 U + 0x02, 0x01, 0x0D, 0x08, // 86 V + 0x02, 0x0E, 0x11, 0x0A, // 87 W + 0x02, 0x1F, 0x0E, 0x08, // 88 X + 0x02, 0x2D, 0x0D, 0x08, // 89 Y + 0x02, 0x3A, 0x0C, 0x07, // 90 Z + 0x02, 0x46, 0x06, 0x04, // 91 [ + 0x02, 0x4C, 0x06, 0x04, // 92 backslash + 0x02, 0x52, 0x04, 0x03, // 93 ] + 0x02, 0x56, 0x09, 0x06, // 94 ^ + 0x02, 0x5F, 0x0C, 0x07, // 95 _ + 0x02, 0x6B, 0x03, 0x03, // 96 ` + 0x02, 0x6E, 0x0A, 0x06, // 97 a + 0x02, 0x78, 0x0A, 0x06, // 98 b + 0x02, 0x82, 0x0A, 0x06, // 99 c + 0x02, 0x8C, 0x0A, 0x06, // 100 d + 0x02, 0x96, 0x0A, 0x06, // 101 e + 0x02, 0xA0, 0x05, 0x04, // 102 f + 0x02, 0xA5, 0x0A, 0x06, // 103 g + 0x02, 0xAF, 0x0A, 0x06, // 104 h + 0x02, 0xB9, 0x04, 0x03, // 105 i + 0x02, 0xBD, 0x04, 0x03, // 106 j + 0x02, 0xC1, 0x08, 0x05, // 107 k + 0x02, 0xC9, 0x04, 0x03, // 108 l + 0x02, 0xCD, 0x10, 0x09, // 109 m + 0x02, 0xDD, 0x0A, 0x06, // 110 n + 0x02, 0xE7, 0x0A, 0x06, // 111 o + 0x02, 0xF1, 0x0A, 0x06, // 112 p + 0x02, 0xFB, 0x0A, 0x06, // 113 q + 0x03, 0x05, 0x05, 0x04, // 114 r + 0x03, 0x0A, 0x08, 0x05, // 115 s + 0x03, 0x12, 0x06, 0x04, // 116 t + 0x03, 0x18, 0x0A, 0x06, // 117 u + 0x03, 0x22, 0x09, 0x06, // 118 v + 0x03, 0x2B, 0x0E, 0x08, // 119 w + 0x03, 0x39, 0x0A, 0x06, // 120 x + 0x03, 0x43, 0x09, 0x06, // 121 y + 0x03, 0x4C, 0x0A, 0x06, // 122 z + 0x03, 0x56, 0x06, 0x04, // 123 { + 0x03, 0x5C, 0x04, 0x03, // 124 | + 0x03, 0x60, 0x05, 0x04, // 125 } + 0x03, 0x65, 0x09, 0x06, // 126 ~ + 0xFF, 0xFF, 0x00, 0x03, // 127 + // Characters 128-191: Placeholders (extended ASCII) + 0xFF, 0xFF, 0x00, 0x03, // 128 + 0xFF, 0xFF, 0x00, 0x03, // 129 + 0xFF, 0xFF, 0x00, 0x03, // 130 + 0xFF, 0xFF, 0x00, 0x03, // 131 + 0xFF, 0xFF, 0x00, 0x03, // 132 + 0xFF, 0xFF, 0x00, 0x03, // 133 + 0xFF, 0xFF, 0x00, 0x03, // 134 + 0xFF, 0xFF, 0x00, 0x03, // 135 + 0xFF, 0xFF, 0x00, 0x03, // 136 + 0xFF, 0xFF, 0x00, 0x03, // 137 + 0xFF, 0xFF, 0x00, 0x03, // 138 + 0xFF, 0xFF, 0x00, 0x03, // 139 + 0xFF, 0xFF, 0x00, 0x03, // 140 + 0xFF, 0xFF, 0x00, 0x03, // 141 + 0xFF, 0xFF, 0x00, 0x03, // 142 + 0xFF, 0xFF, 0x00, 0x03, // 143 + 0xFF, 0xFF, 0x00, 0x03, // 144 + 0xFF, 0xFF, 0x00, 0x03, // 145 + 0xFF, 0xFF, 0x00, 0x03, // 146 + 0xFF, 0xFF, 0x00, 0x03, // 147 + 0xFF, 0xFF, 0x00, 0x03, // 148 + 0xFF, 0xFF, 0x00, 0x03, // 149 + 0xFF, 0xFF, 0x00, 0x03, // 150 + 0xFF, 0xFF, 0x00, 0x03, // 151 + 0xFF, 0xFF, 0x00, 0x03, // 152 + 0xFF, 0xFF, 0x00, 0x03, // 153 + 0xFF, 0xFF, 0x00, 0x03, // 154 + 0xFF, 0xFF, 0x00, 0x03, // 155 + 0xFF, 0xFF, 0x00, 0x03, // 156 + 0xFF, 0xFF, 0x00, 0x03, // 157 + 0xFF, 0xFF, 0x00, 0x03, // 158 + 0xFF, 0xFF, 0x00, 0x03, // 159 + 0xFF, 0xFF, 0x00, 0x03, // 160 + 0xFF, 0xFF, 0x00, 0x03, // 161 + 0xFF, 0xFF, 0x00, 0x03, // 162 + 0xFF, 0xFF, 0x00, 0x03, // 163 + 0xFF, 0xFF, 0x00, 0x03, // 164 + 0xFF, 0xFF, 0x00, 0x03, // 165 + 0xFF, 0xFF, 0x00, 0x03, // 166 + 0xFF, 0xFF, 0x00, 0x03, // 167 + 0xFF, 0xFF, 0x00, 0x03, // 168 + 0xFF, 0xFF, 0x00, 0x03, // 169 + 0xFF, 0xFF, 0x00, 0x03, // 170 + 0xFF, 0xFF, 0x00, 0x03, // 171 + 0xFF, 0xFF, 0x00, 0x03, // 172 + 0xFF, 0xFF, 0x00, 0x03, // 173 + 0xFF, 0xFF, 0x00, 0x03, // 174 + 0xFF, 0xFF, 0x00, 0x03, // 175 + 0xFF, 0xFF, 0x00, 0x03, // 176 + 0xFF, 0xFF, 0x00, 0x03, // 177 + 0xFF, 0xFF, 0x00, 0x03, // 178 + 0xFF, 0xFF, 0x00, 0x03, // 179 + 0xFF, 0xFF, 0x00, 0x03, // 180 + 0xFF, 0xFF, 0x00, 0x03, // 181 + 0xFF, 0xFF, 0x00, 0x03, // 182 + 0xFF, 0xFF, 0x00, 0x03, // 183 + 0xFF, 0xFF, 0x00, 0x03, // 184 + 0xFF, 0xFF, 0x00, 0x03, // 185 + 0xFF, 0xFF, 0x00, 0x03, // 186 + 0xFF, 0xFF, 0x00, 0x03, // 187 + 0xFF, 0xFF, 0x00, 0x03, // 188 + 0xFF, 0xFF, 0x00, 0x03, // 189 + 0xFF, 0xFF, 0x00, 0x03, // 190 + 0xFF, 0xFF, 0x00, 0x03, // 191 + // Characters 192-255: Greek letters (CP-1253 positions) + 0xFF, 0xFF, 0x00, 0x03, // 192 (unused) + 0x03, 0x6E, 0x0E, 0x08, // 193 Α Alpha + 0x03, 0x7C, 0x0C, 0x07, // 194 Β Beta + 0x03, 0x88, 0x09, 0x06, // 195 Γ Gamma + 0x03, 0x91, 0x0C, 0x07, // 196 Δ Delta + 0x03, 0x9D, 0x0C, 0x07, // 197 Ε Epsilon + 0x03, 0xA9, 0x0A, 0x06, // 198 Ζ Zeta + 0x03, 0xB3, 0x0C, 0x07, // 199 Η Eta + 0x03, 0xBF, 0x0E, 0x08, // 200 Θ Theta + 0x03, 0xCD, 0x04, 0x03, // 201 Ι Iota + 0x03, 0xD1, 0x0E, 0x08, // 202 Κ Kappa + 0x03, 0xDF, 0x0E, 0x08, // 203 Λ Lambda + 0x03, 0xED, 0x10, 0x09, // 204 Μ Mu + 0x03, 0xFD, 0x0C, 0x07, // 205 Ν Nu + 0x04, 0x09, 0x0C, 0x07, // 206 Ξ Xi + 0x04, 0x15, 0x0E, 0x08, // 207 Ο Omicron + 0x04, 0x23, 0x0C, 0x07, // 208 Π Pi + 0x04, 0x2F, 0x0B, 0x07, // 209 Ρ Rho + 0xFF, 0xFF, 0x00, 0x03, // 210 (unused) + 0x04, 0x3A, 0x0C, 0x07, // 211 Σ Sigma + 0x04, 0x46, 0x0B, 0x07, // 212 Τ Tau + 0x04, 0x51, 0x0D, 0x08, // 213 Υ Upsilon + 0x04, 0x5E, 0x0E, 0x08, // 214 Φ Phi + 0x04, 0x6C, 0x0E, 0x08, // 215 Χ Chi + 0x04, 0x7A, 0x0E, 0x08, // 216 Ψ Psi + 0x04, 0x88, 0x0E, 0x08, // 217 Ω Omega + 0xFF, 0xFF, 0x00, 0x03, // 218 + 0xFF, 0xFF, 0x00, 0x03, // 219 + 0xFF, 0xFF, 0x00, 0x03, // 220 + 0xFF, 0xFF, 0x00, 0x03, // 221 + 0xFF, 0xFF, 0x00, 0x03, // 222 + 0xFF, 0xFF, 0x00, 0x03, // 223 + 0xFF, 0xFF, 0x00, 0x03, // 224 + 0x04, 0x96, 0x0A, 0x06, // 225 α alpha + 0x04, 0xA0, 0x0A, 0x06, // 226 β beta + 0x04, 0xAA, 0x09, 0x06, // 227 γ gamma + 0x04, 0xB3, 0x0A, 0x06, // 228 δ delta + 0x04, 0xBD, 0x08, 0x05, // 229 ε epsilon + 0x04, 0xC5, 0x08, 0x05, // 230 ζ zeta + 0x04, 0xCD, 0x0A, 0x06, // 231 η eta + 0x04, 0xD7, 0x0A, 0x06, // 232 θ theta + 0x04, 0xE1, 0x04, 0x03, // 233 ι iota + 0x04, 0xE5, 0x08, 0x05, // 234 κ kappa + 0x04, 0xED, 0x0A, 0x06, // 235 λ lambda + 0x04, 0xF7, 0x0A, 0x06, // 236 μ mu + 0x05, 0x01, 0x08, 0x05, // 237 ν nu + 0x05, 0x09, 0x0A, 0x06, // 238 ξ xi + 0x05, 0x13, 0x0A, 0x06, // 239 ο omicron + 0x05, 0x1D, 0x0A, 0x06, // 240 π pi + 0x05, 0x27, 0x0A, 0x06, // 241 ρ rho + 0x05, 0x31, 0x08, 0x05, // 242 ς final sigma + 0x05, 0x39, 0x0A, 0x06, // 243 σ sigma + 0x05, 0x43, 0x06, 0x04, // 244 τ tau + 0x05, 0x49, 0x0A, 0x06, // 245 υ upsilon + 0x05, 0x53, 0x0C, 0x07, // 246 φ phi + 0x05, 0x5F, 0x0A, 0x06, // 247 χ chi + 0x05, 0x69, 0x0C, 0x07, // 248 ψ psi + 0x05, 0x75, 0x0C, 0x07, // 249 ω omega + 0xFF, 0xFF, 0x00, 0x03, // 250 + 0xFF, 0xFF, 0x00, 0x03, // 251 + 0xFF, 0xFF, 0x00, 0x03, // 252 + 0xFF, 0xFF, 0x00, 0x03, // 253 + 0xFF, 0xFF, 0x00, 0x03, // 254 + 0xFF, 0xFF, 0x00, 0x03, // 255 + + // Font Data - Basic ASCII (32-127) + 0x00, 0x00, 0xF8, 0x02, // 33 ! + 0x38, 0x00, 0x00, 0x00, 0x38, // 34 " + 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 # + 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 $ + 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 % + 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 & + 0x38, // 39 ' + 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 ( + 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 ) + 0x28, 0x00, 0x18, 0x00, 0x28, // 42 * + 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 + + 0x00, 0x00, 0x00, 0x06, // 44 , + 0x80, 0x00, 0x80, // 45 - + 0x00, 0x00, 0x00, 0x02, // 46 . + 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 / + 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 0 + 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 1 + 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 2 + 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 3 + 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 4 + 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 5 + 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 6 + 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 7 + 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 8 + 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 9 + 0x00, 0x00, 0x20, 0x02, // 58 : + 0x00, 0x00, 0x20, 0x06, // 59 ; + 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 < + 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 = + 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 > + 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 ? + 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, + 0x04, // 64 @ + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 A + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 B + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 C + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 D + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 E + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 F + 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 G + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 H + 0x00, 0x00, 0xF8, 0x03, // 73 I + 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 J + 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 K + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 L + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 M + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 N + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 O + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 P + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 Q + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 R + 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 S + 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 T + 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 U + 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 V + 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 W + 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 X + 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 Y + 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 Z + 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 [ + 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 backslash + 0x08, 0x08, 0xF8, 0x0F, // 93 ] + 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 ^ + 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 _ + 0x08, 0x00, 0x10, // 96 ` + 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 a + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 b + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 c + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 d + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 e + 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 f + 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 g + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 h + 0x00, 0x00, 0xE8, 0x03, // 105 i + 0x00, 0x08, 0xE8, 0x07, // 106 j + 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 k + 0x00, 0x00, 0xF8, 0x03, // 108 l + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 m + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 n + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 o + 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 p + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 q + 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 r + 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 s + 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 t + 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 u + 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 v + 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 w + 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 x + 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 y + 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 z + 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 { + 0x00, 0x00, 0xF8, 0x0F, // 124 | + 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 } + 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 ~ + + // Greek uppercase letters (193-217 in CP-1253) + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // Α Alpha (same as A) + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // Β Beta (same as B) + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x18, // Γ Gamma + 0x00, 0x02, 0x80, 0x01, 0x60, 0x00, 0x10, 0x00, 0x60, 0x00, 0x80, 0x01, 0x00, 0x02, // Δ Delta + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // Ε Epsilon (same as E) + 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, // Ζ Zeta (same as Z) + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // Η Eta (same as H) + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0x08, 0x02, 0xF0, 0x01, // Θ Theta + 0x00, 0x00, 0xF8, 0x03, // Ι Iota (same as I) + 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // Κ Kappa (same as K) + 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, // Λ Lambda + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // Μ Mu (same as M) + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // Ν Nu (same as N) + 0x00, 0x00, 0x48, 0x02, 0x48, 0x02, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, // Ξ Xi + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // Ο Omicron (same as O) + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // Π Pi + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // Ρ Rho (same as P) + 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // Σ Sigma + 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // Τ Tau (same as T) + 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // Υ Upsilon (same as Y) + 0x00, 0x00, 0x70, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x88, 0x00, 0x70, 0x00, 0x00, // Φ Phi + 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // Χ Chi (same as X) + 0x00, 0x00, 0x08, 0x00, 0xF0, 0x01, 0x08, 0x02, 0xF8, 0x03, 0x08, 0x02, 0xF0, 0x01, // Ψ Psi + 0x00, 0x00, 0x08, 0x02, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, 0x08, 0x02, // Ω Omega + + // Greek lowercase letters (225-249 in CP-1253) + 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // α alpha + 0x00, 0x00, 0xF8, 0x07, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // β beta + 0x00, 0x04, 0x20, 0x02, 0xC0, 0x01, 0x20, 0x00, 0x20, // γ gamma + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x50, 0x01, // δ delta + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, // ε epsilon + 0x00, 0x04, 0x00, 0x03, 0xE0, 0x00, 0x18, // ζ zeta + 0x00, 0x00, 0xE0, 0x05, 0x20, 0x0A, 0x20, 0x02, 0xC0, 0x01, // η eta + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0xA0, 0x02, 0xC0, 0x01, // θ theta + 0x00, 0x00, 0xE0, 0x03, // ι iota + 0xE0, 0x03, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // κ kappa + 0x00, 0x02, 0x80, 0x01, 0x40, 0x00, 0x20, 0x00, 0xE0, 0x03, // λ lambda + 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // μ mu + 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x03, // ν nu + 0x00, 0x04, 0xC0, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, // ξ xi + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // ο omicron + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // π pi + 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // ρ rho + 0x00, 0x04, 0x00, 0x03, 0xA0, 0x02, 0x40, 0x01, // ς final sigma + 0x00, 0x00, 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // σ sigma + 0x20, 0x00, 0xE0, 0x03, 0x20, // τ tau + 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // υ upsilon + 0x00, 0x00, 0xC0, 0x00, 0x20, 0x01, 0xE0, 0x03, 0x20, 0x01, 0xC0, // φ phi + 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // χ chi + 0x00, 0x00, 0x20, 0x00, 0xC0, 0x05, 0x20, 0x02, 0xE0, 0x03, 0x20, // ψ psi + 0x00, 0x00, 0x20, 0x02, 0xC0, 0x01, 0x20, 0x02, 0xC0, 0x01, 0x20, 0x02, // ω omega +}; + +// Placeholder for 16pt font - needs to be generated with font converter tool +const uint8_t ArialMT_Plain_16_GR[] PROGMEM = { + 0x10, // Width: 16 + 0x13, // Height: 19 + 0x20, // First Char: 32 + 0x01, // Number of chars: 1 (placeholder) + // Minimal placeholder - replace with full font data + 0xFF, 0xFF, 0x00, 0x04, // 32 space + // Font Data: + // (empty placeholder) +}; + +// Placeholder for 24pt font - needs to be generated with font converter tool +const uint8_t ArialMT_Plain_24_GR[] PROGMEM = { + 0x18, // Width: 24 + 0x1C, // Height: 28 + 0x20, // First Char: 32 + 0x01, // Number of chars: 1 (placeholder) + // Minimal placeholder - replace with full font data + 0xFF, 0xFF, 0x00, 0x06, // 32 space + // Font Data: + // (empty placeholder) +}; + +#endif // OLED_GR diff --git a/src/graphics/fonts/OLEDDisplayFontsGR.h b/src/graphics/fonts/OLEDDisplayFontsGR.h new file mode 100644 index 000000000..83a2adda6 --- /dev/null +++ b/src/graphics/fonts/OLEDDisplayFontsGR.h @@ -0,0 +1,22 @@ +#ifndef OLEDDISPLAYFONTSGR_h +#define OLEDDISPLAYFONTSGR_h + +#ifdef ARDUINO +#include +#elif __MBED__ +#define PROGMEM +#endif + +/** + * Localization for Greek language containing glyphs for the Greek alphabet. + * Uses Windows-1253 (CP-1253) encoding for Greek characters. + * + * Supported characters: + * - Uppercase Greek: Α-Ω (U+0391 to U+03A9) + * - Lowercase Greek: α-ω (U+03B1 to U+03C9) + * - Accented Greek: ά, έ, ή, ί, ό, ύ, ώ, etc. + */ +extern const uint8_t ArialMT_Plain_10_GR[] PROGMEM; +extern const uint8_t ArialMT_Plain_16_GR[] PROGMEM; +extern const uint8_t ArialMT_Plain_24_GR[] PROGMEM; +#endif diff --git a/src/graphics/images.h b/src/graphics/images.h index c268b3269..ef9ffef78 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -304,58 +304,6 @@ const uint8_t chirpy[] = { 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf}; -#define chirpy_width_hirez 76 -#define chirpy_height_hirez 100 -const uint8_t chirpy_hirez[] = { - 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, - 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, - 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, - 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, - 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, - 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, - 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, - 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, - 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, - 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, - 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, - 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, - 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, - 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, - 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, - 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, - 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, - 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, - 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, - 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, - 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, - 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, - 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, - 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, - 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, - 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, - 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, - 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, - 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, - 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, - 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, - 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, - 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3}; - #define chirpy_small_image_width 8 #define chirpy_small_image_height 8 const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f}; diff --git a/src/graphics/niche/Fonts/FreeSans12pt_Win1253.h b/src/graphics/niche/Fonts/FreeSans12pt_Win1253.h new file mode 100644 index 000000000..a2a41dd1b --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans12pt_Win1253.h @@ -0,0 +1,527 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans12pt_Win1253 +*/ +const uint8_t FreeSans12pt_Win1253Bitmaps[] PROGMEM = { +/* 0x01 */ 0x00, 0x30, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x24, 0x00, 0x04, 0x80, 0x01, 0x90, 0x00, 0x62, 0x00, 0x30, 0xFE, 0x04, 0x10, 0x5F, 0x02, 0x0B, 0x00, 0x7F, 0xE0, 0x0C, 0x1C, 0x02, 0x83, 0x81, 0x9F, 0xF0, 0x02, 0x1E, 0x00, 0x41, 0xC0, 0x0E, 0x7F, 0x81, 0x78, 0x18, 0x62, 0x00, 0xFF, 0xC0, +/* 0x02 */ 0x00, 0xFF, 0x80, 0x61, 0x13, 0xF0, 0x62, 0x60, 0x07, 0xFC, 0x00, 0x83, 0x80, 0x10, 0xF0, 0x33, 0xF6, 0x01, 0x41, 0xC0, 0x18, 0x38, 0x03, 0xFF, 0xE0, 0x47, 0x02, 0x08, 0x20, 0x61, 0xC4, 0x06, 0x17, 0x00, 0x22, 0x00, 0x02, 0x40, 0x00, 0x48, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x3C, 0x00, +/* 0x03 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x04, 0x08, 0x48, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x08, 0x10, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA1, 0x81, 0x8D, 0x87, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x04 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x10, 0x02, 0x48, 0xE0, 0x61, 0xC1, 0xCC, 0x0E, 0x78, 0x1C, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xFC, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x05 */ 0x00, 0x18, 0x00, 0x00, 0x40, 0x01, 0x90, 0x01, 0xF4, 0x08, 0x12, 0x23, 0xC1, 0x91, 0x2C, 0x1C, 0x8A, 0xC3, 0x64, 0x64, 0x13, 0x22, 0x41, 0x98, 0x26, 0x2C, 0xC4, 0x22, 0x60, 0x42, 0x13, 0x04, 0x30, 0x80, 0x61, 0xA4, 0x02, 0x18, 0x20, 0x03, 0x41, 0x00, 0x20, 0x08, 0x02, 0x00, 0x60, 0x40, 0x03, 0xF8, +/* 0x06 */ 0x00, 0x10, 0x00, 0x03, 0x00, 0x1C, 0x48, 0x00, 0xB4, 0x80, 0x09, 0xF9, 0xC0, 0xE0, 0xE4, 0x0C, 0x02, 0x8F, 0x80, 0x38, 0x88, 0x01, 0x0D, 0x00, 0x18, 0x30, 0x01, 0x60, 0x80, 0x13, 0x18, 0x03, 0xF2, 0xC0, 0x20, 0x26, 0x06, 0x07, 0xFF, 0xA0, 0x02, 0x39, 0x00, 0x14, 0x70, 0x01, 0xC3, 0x00, 0x18, 0x00, +/* 0x07 */ +/* 0x08 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x10, 0x37, 0xE2, 0x61, 0x00, 0x0C, 0xC6, 0x10, 0x98, 0x0C, 0x63, 0x00, 0x00, 0xC6, 0x00, +/* 0x09 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x00, 0x37, 0xE0, +/* 0x0A */ +/* 0x0B */ 0x1F, 0x07, 0xC1, 0x86, 0x41, 0x10, 0x0C, 0x04, 0x80, 0x40, 0x18, 0x00, 0x00, 0xC0, 0x00, 0x06, 0x00, 0x00, 0x30, 0x00, 0x01, 0x40, 0x00, 0x0A, 0x00, 0x00, 0x88, 0x00, 0x04, 0x40, 0x00, 0x41, 0x00, 0x02, 0x04, 0x00, 0x20, 0x20, 0x02, 0x00, 0x80, 0x20, 0x02, 0x02, 0x00, 0x08, 0x20, 0x00, 0x22, 0x00, 0x00, 0xE0, 0x00, +/* 0x0C */ 0x01, 0x00, 0x00, 0x38, 0x00, 0x04, 0xC0, 0x01, 0x08, 0x00, 0x18, 0x80, 0x1C, 0x10, 0x02, 0x07, 0x80, 0x81, 0x10, 0x1F, 0xC2, 0x02, 0x00, 0x60, 0x80, 0x1A, 0x20, 0x1C, 0x42, 0x1C, 0x08, 0xFE, 0x03, 0xA0, 0x01, 0x8C, 0x01, 0xC1, 0x43, 0xD0, 0x27, 0x81, 0xF8, +/* 0x0D */ +/* 0x0E */ 0x00, 0xE0, 0x00, 0x11, 0x00, 0x01, 0x10, 0x00, 0x0B, 0x00, 0x03, 0xF8, 0x00, 0x60, 0x60, 0x09, 0x02, 0x00, 0xA0, 0x10, 0x16, 0x01, 0x01, 0x40, 0x10, 0x10, 0x01, 0x01, 0x00, 0x08, 0x10, 0x00, 0x82, 0x1F, 0x08, 0x3F, 0x90, 0x44, 0x00, 0x06, 0xBF, 0xFF, 0xAF, 0xF0, 0xFF, 0xFF, 0x0F, 0xE3, 0xFB, 0xFC, +/* 0x0F */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x40, 0x12, 0x34, 0x00, 0x69, 0x40, 0x01, 0x49, 0xE0, 0xF1, 0xCD, 0x06, 0x8E, 0x28, 0x14, 0x71, 0x40, 0xA3, 0x8B, 0xFD, 0x14, 0x50, 0x68, 0xA2, 0x81, 0x4D, 0x97, 0xFA, 0x44, 0xBF, 0xD6, 0x31, 0x02, 0xE0, 0xC8, 0x16, 0x08, 0x61, 0x08, 0x21, 0xF0, 0x80, 0xF8, 0x78, 0x00, +/* 0x10 */ 0x00, 0xF0, 0x00, 0x3A, 0x00, 0x07, 0xC0, 0x00, 0xA8, 0x00, 0x1F, 0x00, 0x02, 0xB0, 0x00, 0x52, 0x00, 0x0A, 0x40, 0x02, 0x48, 0x00, 0x49, 0x00, 0x09, 0x30, 0x01, 0x22, 0x01, 0xC4, 0x70, 0xF0, 0x85, 0xE1, 0x10, 0x88, 0x37, 0x20, 0x03, 0x9C, 0x00, 0x37, 0x00, 0x06, 0x40, 0x01, 0x86, 0x00, +/* 0x11 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x60, 0x02, 0x36, 0x00, 0x09, 0x04, 0x0C, 0x48, 0x60, 0xC1, 0xC3, 0x0F, 0x0E, 0x00, 0x08, 0x70, 0x00, 0x23, 0x80, 0x63, 0x84, 0x01, 0x9F, 0x20, 0x0C, 0xFD, 0x80, 0x27, 0xE4, 0x03, 0x3F, 0x30, 0x33, 0xE0, 0xC0, 0x00, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x12 */ 0x00, 0xC2, 0x00, 0x1C, 0x24, 0x02, 0x18, 0x60, 0x64, 0x02, 0x02, 0x40, 0x20, 0x00, 0xF2, 0x03, 0x89, 0xE0, 0x7C, 0x80, 0x0E, 0x25, 0x80, 0xE1, 0x00, 0x1A, 0x08, 0x71, 0xB0, 0xC4, 0x39, 0x84, 0xC2, 0xCC, 0x40, 0x76, 0x7C, 0x05, 0xBB, 0x80, 0x4C, 0xE0, 0x0A, 0x78, 0x00, 0x9C, 0x00, 0x0F, 0x00, 0x00, +/* 0x13 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x60, 0xC1, 0xC6, 0xC9, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xF8, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x14 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x20, 0x22, 0x33, 0x01, 0x89, 0x20, 0x02, 0x48, 0x60, 0xE1, 0xC8, 0x80, 0x8E, 0x46, 0x46, 0x72, 0x32, 0x33, 0x9F, 0x9F, 0x94, 0x78, 0x78, 0xA0, 0x00, 0x0D, 0x80, 0x00, 0x44, 0x0E, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x15 */ 0x03, 0xFC, 0x20, 0x38, 0x1C, 0x81, 0x80, 0x1D, 0x08, 0x00, 0x32, 0x60, 0x00, 0x89, 0x00, 0x02, 0x18, 0x00, 0x08, 0x61, 0xC3, 0x22, 0x8D, 0x93, 0x72, 0x00, 0x00, 0x48, 0x00, 0x01, 0x20, 0x00, 0x04, 0x9F, 0xFF, 0x92, 0x60, 0x0E, 0x44, 0xFF, 0xF2, 0x11, 0xC3, 0x88, 0x21, 0xF8, 0x40, 0x40, 0x02, 0x00, 0xC0, 0x30, 0x00, 0xFF, 0x00, +/* 0x16 */ 0x03, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x01, 0xE0, 0x03, 0xF0, 0x03, 0xF0, 0x27, 0xF0, 0x6F, 0x70, 0x6E, 0x60, 0xFC, 0x60, 0xFC, 0x7E, 0xFC, 0x7E, 0xFC, 0x3F, 0xF4, 0x1F, 0xF4, 0x1F, 0xF0, 0x0E, 0x70, 0x0E, 0x30, 0x1C, 0x38, 0x38, 0x0F, 0xF0, +/* 0x17 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x00, 0x21, 0xC0, 0x02, 0x8E, 0x20, 0xF4, 0x70, 0x84, 0x11, 0x82, 0x40, 0x84, 0x01, 0x03, 0x20, 0x0F, 0x85, 0x80, 0x03, 0x04, 0x00, 0x04, 0x30, 0x78, 0x10, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x18 */ 0x00, 0xFC, 0x00, 0x02, 0x06, 0x00, 0x08, 0x24, 0x00, 0x21, 0xA4, 0x00, 0x4C, 0x48, 0x00, 0xA0, 0x50, 0x01, 0x92, 0x60, 0x03, 0x24, 0xC0, 0x06, 0x01, 0x81, 0x28, 0x03, 0x49, 0x6C, 0xC4, 0xAD, 0xD8, 0x16, 0xA4, 0xCC, 0xC4, 0x44, 0x86, 0x13, 0x05, 0x00, 0x28, 0x0A, 0x00, 0x50, 0x14, 0x00, 0x90, 0x48, 0x01, 0x20, 0x90, 0x02, 0x41, 0x20, 0x00, 0x00, +/* 0x19 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x49, 0xC3, 0x81, 0xC0, 0x00, 0x0E, 0x78, 0xF0, 0x77, 0xEF, 0xC3, 0xA7, 0x4E, 0x15, 0x0A, 0x10, 0xA7, 0x8F, 0x0D, 0x80, 0x00, 0x44, 0x00, 0x06, 0x33, 0xF0, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x1A */ 0xFF, 0xFF, 0x00, 0x06, 0x00, 0x0C, 0x3E, 0x18, 0x82, 0x32, 0x02, 0x64, 0x04, 0xC8, 0x09, 0x80, 0x23, 0x00, 0x86, 0x02, 0x0C, 0x08, 0x18, 0x10, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x81, 0x80, 0x03, 0x00, 0x07, 0xFF, 0xF8, +/* 0x1B */ 0x00, 0xFE, 0x00, 0x03, 0x81, 0x80, 0x04, 0x00, 0x60, 0x08, 0x00, 0x30, 0x10, 0x00, 0x10, 0x30, 0x07, 0x88, 0x23, 0xC8, 0x08, 0x22, 0x00, 0x04, 0x60, 0x00, 0x44, 0x60, 0x00, 0x84, 0x63, 0x03, 0x04, 0x61, 0xFC, 0x04, 0x6B, 0x00, 0x9E, 0xA5, 0x01, 0x6A, 0xD5, 0x01, 0x43, 0xA8, 0x81, 0x05, 0xD0, 0x82, 0x0A, 0xA0, 0x82, 0x05, 0xC0, 0x82, 0x02, 0x61, 0xFF, 0x0C, 0x1E, 0x00, 0xF0, +/* 0x1C */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x30, 0x02, 0x32, 0x00, 0x09, 0x00, 0x00, 0x48, 0x20, 0x61, 0xC3, 0x84, 0x0E, 0x1C, 0x78, 0x70, 0x40, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA0, 0x03, 0x0D, 0x83, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x1D */ 0x01, 0xFE, 0x00, 0x3A, 0x1C, 0x03, 0x00, 0x30, 0x23, 0x1E, 0xC3, 0x38, 0x03, 0x10, 0xC3, 0x09, 0x00, 0x18, 0x68, 0x00, 0xC1, 0x40, 0x00, 0x0A, 0x07, 0x80, 0x50, 0x46, 0x02, 0x80, 0x00, 0x1A, 0x1E, 0x00, 0xCB, 0x10, 0x0D, 0x03, 0x00, 0x48, 0x60, 0x06, 0x40, 0x00, 0x22, 0x0C, 0x02, 0x10, 0x60, 0x60, 0x43, 0xFC, 0x01, 0xE0, 0x00, 0x00, +/* 0x1E */ 0x01, 0xF0, 0x00, 0xEA, 0xC0, 0x31, 0x5F, 0x04, 0x5F, 0x88, 0x80, 0xA0, 0x48, 0x0E, 0x02, 0x8F, 0x40, 0x3C, 0x10, 0x21, 0x66, 0x87, 0x15, 0x98, 0x71, 0x41, 0x02, 0x14, 0x00, 0x01, 0x40, 0x00, 0x14, 0x00, 0x01, 0x21, 0xFE, 0x12, 0x00, 0x02, 0x10, 0x00, 0x60, 0x80, 0x0C, 0x06, 0x01, 0x80, 0x3F, 0xE0, +/* 0x1F */ 0x0E, 0x00, 0x13, 0x00, 0x23, 0x00, 0xF3, 0x01, 0x31, 0x01, 0x11, 0x03, 0xD3, 0x06, 0xF2, 0x30, 0x34, 0xC7, 0x25, 0x33, 0x2B, 0xC2, 0x57, 0x04, 0x3A, 0x08, 0x72, 0x30, 0xA3, 0xC3, 0x40, 0x04, 0x40, 0x18, 0x40, 0x60, 0x7F, 0x80, +/* ' ' 0x20 */ +/* '!' 0x21 */ 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, +/* '"' 0x22 */ 0xCF, 0x3C, 0xF3, 0x8A, 0x20, +/* '#' 0x23 */ 0x06, 0x30, 0x31, 0x03, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC0, 0xC6, 0x06, 0x30, +/* '$' 0x24 */ 0x04, 0x03, 0xE1, 0xFF, 0x72, 0x7C, 0x47, 0x88, 0xF1, 0x07, 0xA0, 0x7E, 0x03, 0xF0, 0x17, 0x02, 0x7C, 0x47, 0x88, 0xF1, 0x1B, 0x26, 0x7F, 0xC3, 0xE0, 0x10, 0x02, 0x00, +/* '%' 0x25 */ 0x00, 0x06, 0x03, 0xC0, 0x40, 0x7E, 0x0C, 0x0E, 0x70, 0x80, 0xC3, 0x18, 0x0C, 0x31, 0x00, 0xE7, 0x30, 0x07, 0xE6, 0x00, 0x3C, 0x40, 0x00, 0x0C, 0x7C, 0x00, 0x8F, 0xE0, 0x19, 0xC7, 0x01, 0x18, 0x30, 0x31, 0x83, 0x02, 0x1C, 0x70, 0x40, 0xFE, 0x04, 0x07, 0xC0, +/* '&' 0x26 */ 0x0F, 0x00, 0x7E, 0x03, 0x9C, 0x0C, 0x30, 0x30, 0xC0, 0xE7, 0x01, 0xF8, 0x03, 0x80, 0x3E, 0x01, 0xCC, 0x6E, 0x39, 0xB0, 0x7C, 0xC0, 0xF3, 0x03, 0xCE, 0x1F, 0x9F, 0xE6, 0x3E, 0x1C, +/* ''' 0x27 */ 0xFF, 0xA0, +/* '(' 0x28 */ 0x08, 0x8C, 0x46, 0x31, 0x98, 0xC6, 0x31, 0x8C, 0x63, 0x08, 0x63, 0x08, 0x61, 0x0C, 0x20, +/* ')' 0x29 */ 0x82, 0x18, 0xC3, 0x18, 0xC3, 0x18, 0xC6, 0x31, 0x8C, 0x62, 0x31, 0x88, 0xC4, 0x62, 0x00, +/* '*' 0x2A */ 0x10, 0x23, 0x5B, 0xE3, 0x8D, 0x91, 0x00, +/* '+' 0x2B */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, +/* ',' 0x2C */ 0xF5, 0x60, +/* '-' 0x2D */ 0xFF, 0xF0, +/* '.' 0x2E */ 0xF0, +/* '/' 0x2F */ 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x00, +/* '0' 0x30 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x6C, 0x0F, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3E, 0x0E, 0xC1, 0x9C, 0x71, 0xFC, 0x1F, 0x00, +/* '1' 0x31 */ 0x08, 0xCF, 0xFF, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, +/* '2' 0x32 */ 0x1F, 0x0F, 0xF9, 0x87, 0x60, 0x7C, 0x06, 0x00, 0xC0, 0x18, 0x07, 0x01, 0xC0, 0xF0, 0x78, 0x1C, 0x06, 0x00, 0xC0, 0x30, 0x07, 0xFF, 0xFF, 0xE0, +/* '3' 0x33 */ 0x3F, 0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x0C, 0x01, 0x80, 0x60, 0x78, 0x0F, 0x80, 0x18, 0x01, 0x80, 0x3C, 0x07, 0x80, 0xD8, 0x73, 0xFC, 0x3F, 0x00, +/* '4' 0x34 */ 0x01, 0x80, 0x70, 0x0E, 0x03, 0xC0, 0xD8, 0x1B, 0x06, 0x61, 0x8C, 0x21, 0x8C, 0x33, 0x06, 0x7F, 0xFF, 0xFE, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, +/* '5' 0x35 */ 0x3F, 0xCF, 0xF9, 0x80, 0x30, 0x06, 0x00, 0xDE, 0x1F, 0xE7, 0x0E, 0x00, 0xE0, 0x0C, 0x01, 0x80, 0x30, 0x07, 0x81, 0xB8, 0x73, 0xFC, 0x1F, 0x00, +/* '6' 0x36 */ 0x0F, 0x07, 0xF9, 0xC3, 0x30, 0x74, 0x01, 0x80, 0x33, 0xC7, 0xFE, 0xF1, 0xDC, 0x1F, 0x01, 0xE0, 0x3C, 0x06, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, +/* '7' 0x37 */ 0xFF, 0xFF, 0xFC, 0x01, 0x00, 0x60, 0x18, 0x02, 0x00, 0xC0, 0x30, 0x06, 0x01, 0x80, 0x30, 0x04, 0x01, 0x80, 0x30, 0x06, 0x01, 0x80, 0x30, 0x00, +/* '8' 0x38 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x66, 0x0C, 0xC1, 0x8C, 0x61, 0xF8, 0x3F, 0x8E, 0x3B, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xD8, 0x31, 0xFC, 0x1F, 0x00, +/* '9' 0x39 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x6C, 0x07, 0x80, 0xF0, 0x1E, 0x07, 0x61, 0xEF, 0xFC, 0x79, 0x80, 0x30, 0x05, 0xC1, 0x98, 0x73, 0xFC, 0x1E, 0x00, +/* ':' 0x3A */ 0xF0, 0x00, 0x03, 0xC0, +/* ';' 0x3B */ 0xF0, 0x00, 0x0F, 0x56, +/* '<' 0x3C */ 0x00, 0x70, 0x1E, 0x0F, 0x83, 0xC0, 0xF0, 0x0E, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x10, +/* '=' 0x3D */ 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, +/* '>' 0x3E */ 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C, 0x00, 0xF0, 0x07, 0x01, 0xE0, 0xF0, 0x3C, 0x0F, 0x00, 0x80, 0x00, +/* '?' 0x3F */ 0x3F, 0x1F, 0xEE, 0x1F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x06, 0x03, 0x81, 0xC0, 0xE0, 0x30, 0x0C, 0x03, 0x00, 0x00, 0x00, 0x0C, 0x03, 0x00, +/* '@' 0x40 */ 0x00, 0xFE, 0x00, 0x0F, 0xFE, 0x00, 0xF0, 0x3E, 0x07, 0x00, 0x3C, 0x38, 0x00, 0x38, 0xC1, 0xE0, 0x66, 0x0F, 0xD9, 0xD8, 0x61, 0xC3, 0xC3, 0x07, 0x0F, 0x1C, 0x1C, 0x3C, 0x60, 0x60, 0xF1, 0x81, 0x83, 0xC6, 0x06, 0x1B, 0x18, 0x38, 0xEE, 0x71, 0xE7, 0x18, 0xFD, 0xF8, 0x71, 0xE7, 0xC0, 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xFF, 0xC0, 0x01, 0xFC, 0x00, +/* 'A' 0x41 */ 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, +/* 'B' 0x42 */ 0xFF, 0xC7, 0xFF, 0x30, 0x1D, 0x80, 0x6C, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x0C, 0xFF, 0xC7, 0xFF, 0x30, 0x0D, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x00, +/* 'C' 0x43 */ 0x07, 0xE0, 0x3F, 0xF0, 0xE0, 0x73, 0x80, 0x76, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x00, 0xDC, 0x03, 0x1E, 0x0E, 0x1F, 0xF8, 0x0F, 0xC0, +/* 'D' 0x44 */ 0xFF, 0xC3, 0xFF, 0x8C, 0x07, 0x30, 0x0E, 0xC0, 0x1B, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x1F, 0x00, 0x6C, 0x03, 0xB0, 0x1C, 0xFF, 0xE3, 0xFE, 0x00, +/* 'E' 0x45 */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, +/* 'F' 0x46 */ 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xFF, 0xDF, 0xFB, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x00, +/* 'G' 0x47 */ 0x07, 0xF0, 0x1F, 0xFC, 0x3C, 0x1E, 0x70, 0x07, 0x60, 0x03, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x7F, 0xC0, 0x7F, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x03, 0x60, 0x07, 0x30, 0x0F, 0x3C, 0x1F, 0x1F, 0xFB, 0x07, 0xE1, +/* 'H' 0x48 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xC0, +/* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 'J' 0x4A */ 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x3C, 0x1E, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, +/* 'K' 0x4B */ 0xC0, 0x3E, 0x03, 0xB0, 0x39, 0x83, 0x8C, 0x38, 0x63, 0x83, 0x38, 0x19, 0xC0, 0xDE, 0x07, 0xB8, 0x38, 0xE1, 0x83, 0x0C, 0x1C, 0x60, 0x73, 0x01, 0x98, 0x0E, 0xC0, 0x3E, 0x00, 0xC0, +/* 'L' 0x4C */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFF, 0xFF, 0xF0, +/* 'M' 0x4D */ 0xE0, 0x07, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xD0, 0x0F, 0xD8, 0x1B, 0xD8, 0x1B, 0xD8, 0x1B, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xC6, 0x63, 0xC6, 0x63, 0xC6, 0x63, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC1, 0x83, +/* 'N' 0x4E */ 0xE0, 0x1F, 0x00, 0xFC, 0x07, 0xE0, 0x3D, 0x81, 0xEE, 0x0F, 0x30, 0x79, 0xC3, 0xC6, 0x1E, 0x18, 0xF0, 0xE7, 0x83, 0x3C, 0x1D, 0xE0, 0x6F, 0x01, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xC0, +/* 'O' 0x4F */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x00, 0x18, 0xC0, 0x18, 0x78, 0x3C, 0x1F, 0xFC, 0x03, 0xF8, 0x00, +/* 'P' 0x50 */ 0xFF, 0x8F, 0xFE, 0xC0, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x06, 0xFF, 0xEF, 0xFC, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, +/* 'Q' 0x51 */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x01, 0x98, 0xC0, 0xFC, 0x78, 0x3C, 0x1F, 0xFF, 0x03, 0xF9, 0x80, 0x00, 0x40, +/* 'R' 0x52 */ 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x0C, 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x70, +/* 'S' 0x53 */ 0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x98, 0x07, 0x60, 0x0D, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x0F, 0x80, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xDE, 0x0E, 0x3F, 0xF0, 0x3F, 0x80, +/* 'T' 0x54 */ 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, +/* 'U' 0x55 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x80, 0xEE, 0x0E, 0x3F, 0xE0, 0xFC, 0x00, +/* 'V' 0x56 */ 0xC0, 0x0F, 0x00, 0x7E, 0x01, 0x98, 0x06, 0x60, 0x39, 0xC0, 0xC3, 0x03, 0x0C, 0x1C, 0x38, 0x60, 0x61, 0x81, 0x8E, 0x07, 0x30, 0x0C, 0xC0, 0x37, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1C, 0x00, +/* 'W' 0x57 */ 0xE0, 0x30, 0x1D, 0x80, 0xE0, 0x76, 0x07, 0x81, 0xDC, 0x1E, 0x06, 0x70, 0x7C, 0x18, 0xC1, 0xB0, 0xE3, 0x0C, 0xC3, 0x8C, 0x33, 0x0C, 0x38, 0xC6, 0x30, 0x67, 0x18, 0xC1, 0x98, 0x67, 0x06, 0x61, 0xD8, 0x1D, 0x83, 0x60, 0x3C, 0x0D, 0x80, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0F, 0x01, 0xC0, 0x18, 0x07, 0x00, +/* 'X' 0x58 */ 0xE0, 0x1D, 0x80, 0xE7, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x00, 0xFC, 0x01, 0xE0, 0x07, 0x00, 0x1E, 0x00, 0xF8, 0x03, 0x30, 0x1C, 0xE0, 0xE1, 0x83, 0x07, 0x1C, 0x0E, 0xE0, 0x1B, 0x00, 0x70, +/* 'Y' 0x59 */ 0xC0, 0x0F, 0x80, 0x76, 0x01, 0x9C, 0x0C, 0x38, 0x70, 0x61, 0x81, 0xCE, 0x03, 0x30, 0x0F, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, +/* 'Z' 0x5A */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x03, 0x80, 0x18, 0x01, 0xC0, 0x1C, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, +/* '[' 0x5B */ 0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCF, 0xF0, +/* '\' 0x5C */ 0x81, 0x81, 0x02, 0x06, 0x04, 0x08, 0x18, 0x10, 0x20, 0x60, 0x40, 0x81, 0x81, 0x02, 0x06, 0x04, +/* ']' 0x5D */ 0xFF, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xF0, +/* '^' 0x5E */ 0x0C, 0x0E, 0x05, 0x86, 0xC3, 0x21, 0x19, 0x8C, 0x83, 0xC1, 0x80, +/* '_' 0x5F */ 0xFF, 0xFE, +/* '`' 0x60 */ 0xE3, 0x8C, 0x30, +/* 'a' 0x61 */ 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, +/* 'b' 0x62 */ 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0xF8, 0xDF, 0xCF, 0x0E, 0xE0, 0x7C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xE0, 0x6F, 0x0E, 0xDF, 0xCC, 0xF8, +/* 'c' 0x63 */ 0x1F, 0x0F, 0xE6, 0x1F, 0x83, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x37, 0x1C, 0xFE, 0x1F, 0x00, +/* 'd' 0x64 */ 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x3C, 0xCF, 0xFB, 0x8F, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8F, 0x3F, 0x63, 0xCC, +/* 'e' 0x65 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x3C, 0x07, 0xFF, 0xFF, 0xFE, 0x00, 0xC0, 0x1C, 0x0D, 0xC3, 0x1F, 0xC1, 0xF0, +/* 'f' 0x66 */ 0x3B, 0xD8, 0xC6, 0x7F, 0xEC, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x00, +/* 'g' 0x67 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xB1, 0xE6, 0x00, 0xC0, 0x3E, 0x0E, 0x7F, 0xC7, 0xE0, +/* 'h' 0x68 */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x33, 0xCD, 0xFB, 0xC7, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, +/* 'i' 0x69 */ 0xF0, 0x3F, 0xFF, 0xFF, 0xF0, +/* 'j' 0x6A */ 0x33, 0x00, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xE0, +/* 'k' 0x6B */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0xB8, 0xC6, 0x31, 0xCC, 0x3B, 0x06, 0xC1, 0xF0, 0x30, +/* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 'm' 0x6D */ 0xCF, 0x1F, 0x6F, 0xDF, 0xFC, 0x78, 0xFC, 0x18, 0x3C, 0x0C, 0x1E, 0x06, 0x0F, 0x03, 0x07, 0x81, 0x83, 0xC0, 0xC1, 0xE0, 0x60, 0xF0, 0x30, 0x78, 0x18, 0x3C, 0x0C, 0x18, +/* 'n' 0x6E */ 0xCF, 0x37, 0xEF, 0x1F, 0x83, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, +/* 'o' 0x6F */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x1F, 0xC1, 0xF0, +/* 'p' 0x70 */ 0xCF, 0x8D, 0xFC, 0xF0, 0xEE, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3E, 0x06, 0xF0, 0xEF, 0xFC, 0xCF, 0x8C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, +/* 'q' 0x71 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xF1, 0xE6, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, +/* 'r' 0x72 */ 0xCF, 0x7F, 0x38, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, +/* 's' 0x73 */ 0x3E, 0x1F, 0xEE, 0x1B, 0x00, 0xC0, 0x3C, 0x07, 0xF0, 0x3F, 0x01, 0xF0, 0x3E, 0x1D, 0xFE, 0x3F, 0x00, +/* 't' 0x74 */ 0x63, 0x19, 0xFF, 0xB1, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0xE7, +/* 'u' 0x75 */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x7E, 0x3D, 0xFB, 0x3C, 0xC0, +/* 'v' 0x76 */ 0xE0, 0x6C, 0x0D, 0x81, 0xB8, 0x63, 0x0C, 0x61, 0x8E, 0x60, 0xCC, 0x19, 0x83, 0xE0, 0x3C, 0x07, 0x00, 0xE0, +/* 'w' 0x77 */ 0xC1, 0xC1, 0xB0, 0xE1, 0xD8, 0x70, 0xCC, 0x2C, 0x66, 0x36, 0x31, 0x9B, 0x18, 0xCD, 0x98, 0x64, 0x6C, 0x16, 0x36, 0x0F, 0x1A, 0x07, 0x8F, 0x03, 0x83, 0x80, 0xC1, 0xC0, +/* 'x' 0x78 */ 0xC1, 0xF8, 0x66, 0x30, 0xCC, 0x3E, 0x07, 0x00, 0xC0, 0x78, 0x36, 0x0C, 0xC6, 0x3B, 0x06, 0xC0, 0xC0, +/* 'y' 0x79 */ 0xE0, 0x6C, 0x0D, 0x83, 0x38, 0x63, 0x0C, 0x63, 0x0C, 0x60, 0xCC, 0x1B, 0x03, 0x60, 0x3C, 0x07, 0x00, 0xE0, 0x18, 0x03, 0x00, 0xE0, 0x78, 0x0E, 0x00, +/* 'z' 0x7A */ 0xFF, 0xFF, 0xF0, 0x18, 0x0C, 0x07, 0x03, 0x81, 0xC0, 0x60, 0x30, 0x18, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, +/* '{' 0x7B */ 0x19, 0xCC, 0x63, 0x18, 0xC6, 0x31, 0x99, 0x86, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x1C, 0x60, +/* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, +/* '}' 0x7D */ 0xC7, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x0C, 0x33, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x73, 0x00, +/* '~' 0x7E */ 0x70, 0x3E, 0x09, 0xE4, 0x1F, 0x03, 0x80, +/* 0x7F */ +/* 0x80 */ 0x01, 0xF0, 0x1F, 0xF0, 0xE0, 0xC7, 0x00, 0x18, 0x00, 0xC0, 0x07, 0xFF, 0x3F, 0xFC, 0x30, 0x01, 0xFF, 0x8F, 0xFC, 0x0C, 0x00, 0x18, 0x00, 0x70, 0x00, 0xE0, 0x81, 0xFE, 0x03, 0xF0, +/* 0x81 */ +/* 0x82 */ 0xF5, 0x80, +/* 0x83 */ 0x1C, 0xF3, 0x0C, 0x31, 0xF7, 0xCC, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x33, 0xCE, 0x00, +/* 0x84 */ 0xCF, 0x34, 0x51, 0x88, +/* 0x85 */ 0xC6, 0x3C, 0x63, +/* 0x86 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, +/* 0x87 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x0F, 0xFF, 0xFF, 0x0C, 0x03, 0x00, 0xC0, 0x30, +/* 0x88 */ 0x38, 0xD9, 0xB6, 0x30, +/* 0x89 */ 0x38, 0x18, 0x00, 0xF8, 0x30, 0x03, 0x18, 0xC0, 0x04, 0x11, 0x80, 0x0C, 0x66, 0x00, 0x0F, 0x8C, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x40, 0x00, 0x01, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x31, 0xC0, 0xE0, 0x67, 0xC3, 0xC1, 0x98, 0xCC, 0xC3, 0x20, 0x90, 0x8C, 0x63, 0x33, 0x10, 0x7C, 0x3C, 0x60, 0x70, 0x38, +/* 0x8A */ 0x0C, 0x40, 0x1F, 0x00, 0x38, 0x03, 0xF8, 0x1F, 0xF0, 0xE0, 0xE6, 0x01, 0xD8, 0x03, 0x60, 0x01, 0xC0, 0x07, 0x80, 0x0F, 0xE0, 0x0F, 0xF0, 0x03, 0xE0, 0x01, 0xF0, 0x03, 0xC0, 0x0F, 0x80, 0x37, 0x83, 0x8F, 0xFC, 0x0F, 0xE0, +/* 0x8B */ 0x2F, 0x49, 0x99, +/* 0x8C */ 0x07, 0xCF, 0xFC, 0x7F, 0xFF, 0xF3, 0x83, 0xC0, 0x18, 0x07, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x30, 0x0C, 0x00, 0xC0, 0x30, 0x03, 0x00, 0xC0, 0x0F, 0xFF, 0x00, 0x3F, 0xFC, 0x00, 0xC0, 0x30, 0x03, 0x00, 0xC0, 0x0C, 0x01, 0x80, 0x30, 0x07, 0x01, 0xC0, 0x0E, 0x0F, 0x00, 0x1F, 0xEF, 0xFC, 0x1F, 0x3F, 0xF0, +/* 0x8D */ +/* 0x8E */ 0x0C, 0xC0, 0x3C, 0x00, 0xE1, 0xFF, 0xFF, 0xFF, 0x80, 0x1C, 0x01, 0xC0, 0x1C, 0x00, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x01, 0xFF, 0xFF, 0xFF, 0x80, +/* 0x8F */ +/* 0x90 */ +/* 0x91 */ 0x6A, 0xF0, +/* 0x92 */ 0xF5, 0x60, +/* 0x93 */ 0x4E, 0x28, 0xA2, 0xCF, 0x30, +/* 0x94 */ 0xCF, 0x34, 0x51, 0x4E, 0x20, +/* 0x95 */ 0x7B, 0xFF, 0xFF, 0xFD, 0xE0, +/* 0x96 */ 0xFF, 0xFF, 0xF0, +/* 0x97 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 0x98 */ 0x63, 0xFE, 0x70, +/* 0x99 */ 0xFF, 0x70, 0x1F, 0xFD, 0xC0, 0x71, 0x87, 0x83, 0xC6, 0x1E, 0x0F, 0x18, 0x68, 0x3C, 0x61, 0xB1, 0xB1, 0x86, 0xC6, 0xC6, 0x19, 0x1B, 0x18, 0x66, 0xCC, 0x61, 0x9B, 0x31, 0x86, 0x3C, 0xC6, 0x18, 0xE3, 0x18, 0x63, 0x8C, +/* 0x9A */ 0x63, 0x0D, 0x83, 0x60, 0x70, 0x00, 0x0F, 0x87, 0xFB, 0x86, 0xC0, 0x30, 0x0F, 0x01, 0xFC, 0x0F, 0xC0, 0x7C, 0x0F, 0x87, 0x7F, 0x8F, 0xC0, +/* 0x9B */ 0x99, 0x92, 0xF4, +/* 0x9C */ 0x1F, 0x0F, 0x83, 0xF9, 0xFC, 0x71, 0xF8, 0x6E, 0x0F, 0x03, 0xC0, 0x60, 0x3C, 0x07, 0xFF, 0xC0, 0x7F, 0xFC, 0x06, 0x00, 0xC0, 0x60, 0x0E, 0x0F, 0x03, 0x71, 0xF8, 0x63, 0xF9, 0xFC, 0x1F, 0x0F, 0x80, +/* 0x9D */ +/* 0x9E */ 0x63, 0x0C, 0x83, 0x60, 0x70, 0x00, 0x3F, 0xFF, 0xFC, 0x06, 0x03, 0x01, 0xC0, 0xE0, 0x70, 0x18, 0x0C, 0x06, 0x03, 0x80, 0xFF, 0xFF, 0xF0, +/* 0x9F */ 0x0C, 0xC0, 0x33, 0x00, 0x00, 0x30, 0x03, 0xE0, 0x1D, 0x80, 0x67, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x80, 0xCC, 0x03, 0xE0, 0x07, 0x80, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, +/* 0xA0 */ +/* 0xA1 */ 0xF0, 0xBF, 0xFF, 0xFF, 0xF0, +/* 0xA2 */ 0x04, 0x00, 0x80, 0x7C, 0x1F, 0xE7, 0x4C, 0xC8, 0xF1, 0x1E, 0x20, 0xC4, 0x18, 0x83, 0x10, 0x72, 0x37, 0x4E, 0x7F, 0x87, 0xC0, 0x20, 0x04, 0x00, +/* 0xA3 */ 0x0F, 0xC1, 0xFE, 0x38, 0x76, 0x03, 0x60, 0x36, 0x00, 0x70, 0x03, 0x80, 0xFF, 0x0F, 0xF0, 0x1C, 0x00, 0xC0, 0x0C, 0x01, 0x80, 0x10, 0x02, 0xF1, 0x7F, 0xF6, 0x1F, +/* 0xA4 */ 0xDD, 0xFF, 0xD8, 0xD8, 0x3C, 0x1E, 0x0F, 0x8D, 0xFF, 0xDD, 0x80, +/* 0xA5 */ 0xC0, 0x3E, 0x06, 0x60, 0x63, 0x0C, 0x30, 0xC1, 0x98, 0x19, 0x80, 0xF0, 0x0F, 0x07, 0xFE, 0x06, 0x00, 0x60, 0x7F, 0xE0, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, +/* 0xA6 */ 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFC, +/* 0xA7 */ 0x0F, 0x03, 0xF0, 0xE7, 0x18, 0x63, 0x0C, 0x70, 0x07, 0x03, 0xF8, 0xC3, 0x98, 0x3B, 0x03, 0xF0, 0x37, 0x06, 0x78, 0xC7, 0xB0, 0x7C, 0x03, 0x80, 0x39, 0x83, 0x30, 0x67, 0x1C, 0x7F, 0x07, 0xC0, +/* 0xA8 */ 0xCF, 0x30, +/* 0xA9 */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xE3, 0x1C, 0x73, 0xF3, 0x99, 0x86, 0x6C, 0xC1, 0x8F, 0x30, 0x03, 0xCC, 0x00, 0xF3, 0x00, 0x3C, 0xC1, 0x8D, 0x98, 0x66, 0x77, 0xF3, 0x8E, 0x79, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, +/* 0xAA */ 0x79, 0x08, 0x11, 0xEE, 0x50, 0xA3, 0x3B, 0x00, 0x03, 0xF8, +/* 0xAB */ 0x21, 0x63, 0xE7, 0x84, 0x84, 0xE7, 0x63, 0x21, +/* 0xAC */ 0xFF, 0xFF, 0xFF, 0x00, 0x30, 0x03, 0x00, 0x30, 0x03, +/* 0xAD */ 0xFF, 0xF0, +/* 0xAE */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xFF, 0x1C, 0x7F, 0xF3, 0x9B, 0x04, 0x6C, 0xC1, 0x8F, 0x30, 0x43, 0xCF, 0xF0, 0xF3, 0xFC, 0x3C, 0xC1, 0x0D, 0xB0, 0x66, 0x7C, 0x1B, 0x8F, 0x07, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, +/* 0xAF */ 0xFF, 0xF0, +/* 0xB0 */ 0x38, 0xFB, 0x1C, 0x18, 0x38, 0xDF, 0x1C, +/* 0xB1 */ 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x7F, 0xE7, 0xFE, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, +/* 0xB2 */ 0x7D, 0x8F, 0x18, 0x30, 0xC6, 0x18, 0x60, 0xFF, 0xFC, +/* 0xB3 */ 0x7D, 0x8F, 0x18, 0x31, 0x80, 0xC1, 0xE3, 0xC6, 0xF8, +/* 0xB4 */ 0x3B, 0x99, 0x80, +/* 0xB5 */ 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x1C, 0xE3, 0xCF, 0xEF, 0xFC, 0x7C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, +/* 0xB6 */ 0x1F, 0xE7, 0xFD, 0xF3, 0x7E, 0x6F, 0xCD, 0xF9, 0xBF, 0x37, 0xE6, 0x7C, 0xCF, 0x98, 0xF3, 0x06, 0x60, 0xCC, 0x19, 0x83, 0x30, 0x66, 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x60, 0xCC, +/* 0xB7 */ 0xF0, +/* 0xB8 */ 0x10, 0xF0, 0xE3, 0x78, +/* 0xB9 */ 0x2F, 0xB6, 0xDB, 0x6C, +/* 0xBA */ 0x79, 0x38, 0x61, 0x86, 0x1C, 0xDE, 0x00, 0x0F, 0xC0, +/* 0xBB */ 0x88, 0xC6, 0xE7, 0x21, 0x21, 0xE7, 0xC6, 0x88, +/* 0xBC */ 0x20, 0x08, 0x30, 0x0C, 0x38, 0x04, 0x0C, 0x06, 0x06, 0x02, 0x03, 0x02, 0x01, 0x81, 0x00, 0xC1, 0x06, 0x61, 0x87, 0x30, 0x83, 0x80, 0xC2, 0xC0, 0x42, 0x60, 0x43, 0x30, 0x21, 0xFC, 0x20, 0x0C, 0x30, 0x06, 0x10, 0x03, 0x00, +/* 0xBD */ 0x20, 0x00, 0x08, 0x02, 0x06, 0x01, 0x83, 0x80, 0x40, 0x60, 0x20, 0x18, 0x18, 0x06, 0x04, 0x01, 0x83, 0x00, 0x61, 0x9F, 0x98, 0x4E, 0x76, 0x33, 0x0C, 0x08, 0x03, 0x04, 0x03, 0x83, 0x01, 0x80, 0x81, 0x80, 0x60, 0xC0, 0x30, 0x3F, 0xC8, 0x0F, 0xF0, +/* 0xBE */ 0x7C, 0x00, 0x18, 0xC0, 0x43, 0x18, 0x18, 0x03, 0x02, 0x00, 0x60, 0xC0, 0x30, 0x10, 0x01, 0x84, 0x00, 0x31, 0x80, 0xC6, 0x20, 0xD8, 0xC8, 0x39, 0xF1, 0x07, 0x00, 0x41, 0x60, 0x18, 0x4C, 0x02, 0x11, 0x80, 0x83, 0xF8, 0x10, 0x06, 0x04, 0x00, 0xC1, 0x00, 0x18, +/* 0xBF */ 0x0C, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x60, 0x30, 0x30, 0x38, 0x38, 0x18, 0x0C, 0x06, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, +/* 0xC0 */ 0x0C, 0xDB, 0xD3, 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, +/* 0xC1 */ 0x03, 0x80, 0x07, 0x00, 0x1B, 0x00, 0x36, 0x00, 0xEE, 0x01, 0x8C, 0x03, 0x18, 0x0C, 0x18, 0x18, 0x30, 0x30, 0x60, 0xFF, 0xE1, 0xFF, 0xC7, 0x01, 0xCC, 0x01, 0x98, 0x03, 0x60, 0x03, 0xC0, 0x06, +/* 0xC2 */ 0xFF, 0x87, 0xFF, 0x30, 0x1D, 0x80, 0x6C, 0x03, 0x60, 0x1B, 0x01, 0x9F, 0xFC, 0xFF, 0xE6, 0x03, 0xB0, 0x0F, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0xDF, 0xFE, 0xFF, 0xC0, +/* 0xC3 */ 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x00, +/* 0xC4 */ 0x01, 0xC0, 0x01, 0xC0, 0x03, 0x60, 0x03, 0x60, 0x07, 0x60, 0x06, 0x30, 0x06, 0x30, 0x0C, 0x18, 0x0C, 0x18, 0x1C, 0x18, 0x18, 0x0C, 0x18, 0x0C, 0x30, 0x06, 0x30, 0x06, 0x70, 0x06, 0x7F, 0xFF, 0x7F, 0xFF, +/* 0xC5 */ 0xFF, 0xFF, 0xFF, 0xF0, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x1F, 0xFE, 0xFF, 0xF6, 0x00, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x1F, 0xFF, 0xFF, 0xF8, +/* 0xC6 */ 0x7F, 0xFD, 0xFF, 0xF0, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x80, 0x1C, 0x00, 0x60, 0x03, 0x00, 0x18, 0x00, 0xE0, 0x07, 0x00, 0x18, 0x00, 0xC0, 0x06, 0x00, 0x3F, 0xFF, 0xFF, 0xFC, +/* 0xC7 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x7F, 0xFF, 0xFF, 0xFE, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x18, +/* 0xC8 */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x8E, 0x00, 0xE6, 0x00, 0x37, 0x00, 0x1F, 0x00, 0x07, 0x9F, 0xF3, 0xCF, 0xF9, 0xE0, 0x00, 0xF0, 0x00, 0x7C, 0x00, 0x76, 0x00, 0x33, 0x80, 0x38, 0xF0, 0x78, 0x3F, 0xF8, 0x07, 0xF0, 0x00, +/* 0xC9 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, +/* 0xCA */ 0xC0, 0x3B, 0x01, 0xCC, 0x0E, 0x30, 0x70, 0xC3, 0x83, 0x1C, 0x0C, 0xE0, 0x37, 0x80, 0xFF, 0x03, 0xDC, 0x0E, 0x38, 0x30, 0x70, 0xC0, 0xE3, 0x03, 0x8C, 0x07, 0x30, 0x0E, 0xC0, 0x1C, +/* 0xCB */ 0x01, 0xC0, 0x00, 0xE0, 0x00, 0xD8, 0x00, 0x6C, 0x00, 0x37, 0x00, 0x31, 0x80, 0x18, 0xC0, 0x18, 0x30, 0x0C, 0x18, 0x0E, 0x0E, 0x06, 0x03, 0x03, 0x01, 0x83, 0x00, 0x61, 0x80, 0x31, 0xC0, 0x1C, 0xC0, 0x06, 0x60, 0x03, 0x00, +/* 0xCC */ 0xE0, 0x0F, 0xE0, 0x3F, 0xC0, 0x7F, 0x80, 0xFD, 0x83, 0x7B, 0x06, 0xF6, 0x0D, 0xE4, 0x13, 0xCC, 0x67, 0x98, 0xCF, 0x31, 0x9E, 0x36, 0x3C, 0x6C, 0x78, 0xD8, 0xF0, 0xA1, 0xE1, 0xC3, 0xC3, 0x86, +/* 0xCD */ 0xC0, 0x1F, 0x00, 0xFC, 0x07, 0xE0, 0x3D, 0x81, 0xEE, 0x0F, 0x30, 0x78, 0xC3, 0xC7, 0x1E, 0x18, 0xF0, 0x67, 0x83, 0xBC, 0x0D, 0xE0, 0x3F, 0x01, 0xF8, 0x07, 0xC0, 0x18, +/* 0xCE */ 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFE, 0x7F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFC, +/* 0xCF */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x8E, 0x00, 0xE6, 0x00, 0x37, 0x00, 0x1F, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x7C, 0x00, 0x76, 0x00, 0x33, 0x80, 0x38, 0xF0, 0x78, 0x3F, 0xF8, 0x07, 0xF0, 0x00, +/* 0xD0 */ 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x18, +/* 0xD1 */ 0xFF, 0xC7, 0xFF, 0xB0, 0x0D, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x18, 0x00, 0xC0, 0x00, +/* 0xD2 */ +/* 0xD3 */ 0xFF, 0xEF, 0xFE, 0xC0, 0x06, 0x00, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x0F, 0xFF, 0xFF, 0xF0, +/* 0xD4 */ 0xFF, 0xFF, 0xFF, 0xF0, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, +/* 0xD5 */ 0xE0, 0x07, 0x60, 0x0E, 0x30, 0x1C, 0x38, 0x1C, 0x1C, 0x38, 0x0E, 0x70, 0x06, 0x60, 0x07, 0xE0, 0x03, 0xC0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, +/* 0xD6 */ 0x01, 0x80, 0x01, 0x80, 0x0F, 0xF0, 0x3F, 0xFC, 0x71, 0x8E, 0x61, 0x86, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0x61, 0x86, 0x71, 0x8E, 0x3F, 0xFC, 0x0F, 0xF0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, +/* 0xD7 */ 0x70, 0x1C, 0x70, 0x70, 0x61, 0xC0, 0xE3, 0x80, 0xEE, 0x00, 0xD8, 0x01, 0xF0, 0x01, 0xC0, 0x03, 0x80, 0x0D, 0x80, 0x3B, 0x80, 0x77, 0x01, 0xC7, 0x07, 0x07, 0x0E, 0x06, 0x38, 0x0E, 0xE0, 0x0E, +/* 0xD8 */ 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0x61, 0x86, 0x79, 0x9E, 0x3F, 0xFC, 0x0F, 0xF0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, +/* 0xD9 */ 0x07, 0xE0, 0x1F, 0xF8, 0x38, 0x3C, 0x70, 0x0E, 0x60, 0x06, 0xE0, 0x07, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x06, 0x60, 0x06, 0x30, 0x0C, 0x1C, 0x38, 0xFE, 0x7F, 0xFE, 0x7F, +/* 0xDA */ 0xCF, 0x30, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, +/* 0xDB */ 0x06, 0x60, 0x06, 0x60, 0x00, 0x00, 0xE0, 0x07, 0x60, 0x0E, 0x30, 0x1C, 0x38, 0x1C, 0x1C, 0x38, 0x0E, 0x70, 0x06, 0x60, 0x07, 0xE0, 0x03, 0xC0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, +/* 0xDC */ 0x03, 0x80, 0x30, 0x06, 0x00, 0x00, 0x1E, 0x33, 0xFB, 0x71, 0xFE, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x6E, 0x0E, 0x71, 0xF3, 0xFF, 0x1F, 0x30, +/* 0xDD */ 0x0C, 0x0C, 0x04, 0x00, 0x03, 0xE3, 0xFF, 0x8D, 0x80, 0xE0, 0x3E, 0x1F, 0x1C, 0x0C, 0x06, 0x0B, 0x8E, 0xFE, 0x3E, 0x00, +/* 0xDE */ 0x07, 0x01, 0x80, 0xC0, 0x00, 0xCF, 0x3F, 0xEE, 0x1F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, +/* 0xDF */ 0x76, 0xC0, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x60, +/* 0xE0 */ 0x0C, 0x1B, 0x66, 0x98, 0x00, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x36, 0x19, 0xFE, 0x1E, 0x00, +/* 0xE1 */ 0x1E, 0x33, 0xFB, 0x71, 0xFE, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x6E, 0x0E, 0x71, 0xF3, 0xFF, 0x1F, 0x30, +/* 0xE2 */ 0x1F, 0x0F, 0xF1, 0x87, 0x60, 0x6C, 0x0D, 0x83, 0x33, 0x86, 0x7C, 0xC1, 0xD8, 0x1F, 0x01, 0xE0, 0x3C, 0x07, 0xC0, 0xFC, 0x36, 0xFE, 0xCF, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x00, +/* 0xE3 */ 0x60, 0x66, 0x06, 0x60, 0x63, 0x0C, 0x30, 0xC3, 0x8C, 0x19, 0x81, 0x98, 0x1F, 0x80, 0xF0, 0x0F, 0x00, 0xF0, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, +/* 0xE4 */ 0x7F, 0xCF, 0xF8, 0xE0, 0x07, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x07, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0xC1, 0x9C, 0x71, 0xFC, 0x1F, 0x00, +/* 0xE5 */ 0x3E, 0x3F, 0xF8, 0xD8, 0x0E, 0x03, 0xE1, 0xF1, 0xC0, 0xC0, 0x60, 0xB8, 0xEF, 0xE3, 0xE0, +/* 0xE6 */ 0x3F, 0x9F, 0xC0, 0xC1, 0xC1, 0xC0, 0xC0, 0xC0, 0xC0, 0x60, 0x70, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x80, 0xFC, 0x3F, 0x80, 0xC0, 0x60, 0x70, 0xF0, 0x70, +/* 0xE7 */ 0xCF, 0x3F, 0xEE, 0x1F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, +/* 0xE8 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x6C, 0x07, 0x80, 0xF0, 0x1F, 0xFF, 0xFF, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x0E, 0xC1, 0x9C, 0x71, 0xFC, 0x1F, 0x00, +/* 0xE9 */ 0xFF, 0xFF, 0xFF, 0xC0, +/* 0xEA */ 0xC1, 0xB0, 0xCC, 0x63, 0x30, 0xD8, 0x3C, 0x0F, 0x83, 0x60, 0xCC, 0x31, 0x8C, 0x73, 0x0C, 0xC1, 0x80, +/* 0xEB */ 0x0C, 0x00, 0x60, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x01, 0x80, 0x1C, 0x00, 0xF0, 0x0D, 0x80, 0x6C, 0x06, 0x30, 0x31, 0x83, 0x8E, 0x18, 0x30, 0xC1, 0x8C, 0x06, 0x60, 0x30, +/* 0xEC */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF8, 0x7E, 0x1F, 0xFF, 0xDE, 0xF0, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x00, +/* 0xED */ 0xC0, 0x78, 0x0D, 0x83, 0x30, 0x66, 0x0C, 0x63, 0x0C, 0x60, 0xD8, 0x1B, 0x03, 0x60, 0x38, 0x07, 0x00, 0x40, +/* 0xEE */ 0x1F, 0x1F, 0x9C, 0x0C, 0x07, 0x01, 0xF8, 0x3C, 0x70, 0x70, 0x30, 0x30, 0x18, 0x0C, 0x06, 0x01, 0xC0, 0xFC, 0x1F, 0x00, 0xC0, 0x60, 0x70, 0xF0, 0x70, +/* 0xEF */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x1F, 0xC1, 0xF0, +/* 0xF0 */ 0xFF, 0xFF, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, +/* 0xF1 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x6C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1F, 0xC7, 0x7F, 0xCD, 0xF1, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x00, +/* 0xF2 */ 0x07, 0xE3, 0xFC, 0xE0, 0x30, 0x06, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x1C, 0x01, 0xC0, 0x1F, 0x80, 0xF8, 0x03, 0x80, 0x30, 0x0E, 0x0F, 0x81, 0xE0, +/* 0xF3 */ 0x1F, 0xF9, 0xFF, 0xDC, 0x39, 0xC0, 0xCC, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x06, 0xC0, 0x37, 0x03, 0x9C, 0x38, 0x7F, 0x81, 0xF8, 0x00, +/* 0xF4 */ 0xFF, 0xF3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, +/* 0xF5 */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x36, 0x19, 0xFE, 0x1E, 0x00, +/* 0xF6 */ 0x19, 0xE0, 0xEF, 0xC6, 0x31, 0xB8, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xE3, 0x1D, 0x8C, 0x67, 0xB7, 0x0F, 0xF8, 0x0F, 0xC0, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, +/* 0xF7 */ 0x60, 0x33, 0x03, 0x8C, 0x18, 0x71, 0xC1, 0x8C, 0x0E, 0xC0, 0x36, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x1B, 0x00, 0xDC, 0x0C, 0x60, 0xE3, 0x86, 0x0C, 0x70, 0x33, 0x01, 0x80, +/* 0xF8 */ 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x8C, 0x77, 0x33, 0x8F, 0xFC, 0x1F, 0xE0, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, +/* 0xF9 */ 0x30, 0x0C, 0x60, 0x06, 0x60, 0x06, 0xE1, 0x87, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xE1, 0x87, 0x63, 0xC6, 0x7E, 0x7E, 0x3C, 0x38, +/* 0xFA */ 0xCF, 0x30, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, +/* 0xFB */ 0x33, 0x0C, 0xC0, 0x03, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xD8, 0x67, 0xF8, 0x78, +/* 0xFC */ 0x07, 0x00, 0xC0, 0x30, 0x00, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x07, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, +/* 0xFD */ 0x06, 0x03, 0x00, 0x80, 0x00, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x36, 0x19, 0xFE, 0x1E, 0x00, +/* 0xFE */ 0x00, 0xC0, 0x01, 0x80, 0x01, 0x00, 0x00, 0x00, 0x30, 0x0C, 0x60, 0x06, 0x60, 0x06, 0xE1, 0x87, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xE1, 0x87, 0x63, 0xC6, 0x7E, 0x7E, 0x3C, 0x38, +/* 0xFF */ +}; + +const GFXglyph FreeSans12pt_Win1253Glyphs[] PROGMEM = { +/* 0x01 */ { 0, 19, 20, 21, 1, -17 }, +/* 0x02 */ { 48, 19, 20, 21, 1, -17 }, +/* 0x03 */ { 96, 21, 20, 23, 1, -17 }, +/* 0x04 */ { 149, 21, 20, 23, 1, -17 }, +/* 0x05 */ { 202, 20, 20, 22, 1, -17 }, +/* 0x06 */ { 252, 20, 20, 22, 1, -17 }, +/* 0x07 */ { 302, 0, 0, 8, 0, 0 }, +/* 0x08 */ { 302, 23, 20, 25, 1, -17 }, +/* 0x09 */ { 360, 23, 16, 25, 1, -16 }, +/* 0x0A */ { 406, 0, 0, 8, 0, 0 }, +/* 0x0B */ { 406, 21, 20, 23, 1, -17 }, +/* 0x0C */ { 459, 19, 18, 21, 1, -15 }, +/* 0x0D */ { 502, 0, 0, 8, 0, 0 }, +/* 0x0E */ { 502, 20, 20, 22, 1, -17 }, +/* 0x0F */ { 552, 21, 21, 23, 1, -18 }, +/* 0x10 */ { 608, 19, 20, 21, 1, -17 }, +/* 0x11 */ { 656, 21, 20, 23, 1, -17 }, +/* 0x12 */ { 709, 20, 20, 22, 1, -17 }, +/* 0x13 */ { 759, 21, 20, 23, 1, -17 }, +/* 0x14 */ { 812, 21, 20, 23, 1, -17 }, +/* 0x15 */ { 865, 22, 20, 24, 1, -17 }, +/* 0x16 */ { 920, 16, 20, 18, 1, -17 }, +/* 0x17 */ { 960, 21, 20, 23, 1, -17 }, +/* 0x18 */ { 1013, 23, 20, 25, 1, -17 }, +/* 0x19 */ { 1071, 21, 20, 23, 1, -17 }, +/* 0x1A */ { 1124, 15, 19, 17, 1, -16 }, +/* 0x1B */ { 1160, 24, 21, 26, 1, -18 }, +/* 0x1C */ { 1223, 21, 20, 23, 1, -17 }, +/* 0x1D */ { 1276, 21, 21, 23, 1, -18 }, +/* 0x1E */ { 1332, 20, 20, 22, 1, -17 }, +/* 0x1F */ { 1382, 15, 20, 17, 1, -17 }, +/* ' ' 0x20 */ { 1420, 0, 0, 6, 0, 0 }, +/* '!' 0x21 */ { 1420, 2, 18, 8, 3, -16 }, +/* '"' 0x22 */ { 1425, 6, 6, 8, 1, -15 }, +/* '#' 0x23 */ { 1430, 13, 16, 13, 0, -14 }, +/* '$' 0x24 */ { 1456, 11, 20, 13, 1, -16 }, +/* '%' 0x25 */ { 1484, 20, 17, 21, 1, -15 }, +/* '&' 0x26 */ { 1527, 14, 17, 16, 1, -15 }, +/* ''' 0x27 */ { 1557, 2, 6, 5, 1, -15 }, +/* '(' 0x28 */ { 1559, 5, 23, 8, 2, -16 }, +/* ')' 0x29 */ { 1574, 5, 23, 8, 1, -16 }, +/* '*' 0x2A */ { 1589, 7, 7, 9, 1, -16 }, +/* '+' 0x2B */ { 1596, 10, 11, 14, 2, -9 }, +/* ',' 0x2C */ { 1610, 2, 6, 7, 2, 0 }, +/* '-' 0x2D */ { 1612, 6, 2, 8, 1, -6 }, +/* '.' 0x2E */ { 1614, 2, 2, 6, 2, 0 }, +/* '/' 0x2F */ { 1615, 7, 18, 7, 0, -16 }, +/* '0' 0x30 */ { 1631, 11, 17, 13, 1, -15 }, +/* '1' 0x31 */ { 1655, 5, 17, 13, 3, -15 }, +/* '2' 0x32 */ { 1666, 11, 17, 13, 1, -15 }, +/* '3' 0x33 */ { 1690, 11, 17, 13, 1, -15 }, +/* '4' 0x34 */ { 1714, 11, 17, 13, 1, -15 }, +/* '5' 0x35 */ { 1738, 11, 17, 13, 1, -15 }, +/* '6' 0x36 */ { 1762, 11, 17, 13, 1, -15 }, +/* '7' 0x37 */ { 1786, 11, 17, 13, 1, -15 }, +/* '8' 0x38 */ { 1810, 11, 17, 13, 1, -15 }, +/* '9' 0x39 */ { 1834, 11, 17, 13, 1, -15 }, +/* ':' 0x3A */ { 1858, 2, 13, 6, 2, -11 }, +/* ';' 0x3B */ { 1862, 2, 16, 6, 2, -10 }, +/* '<' 0x3C */ { 1866, 12, 11, 14, 1, -9 }, +/* '=' 0x3D */ { 1883, 12, 6, 14, 1, -7 }, +/* '>' 0x3E */ { 1892, 12, 11, 14, 1, -9 }, +/* '?' 0x3F */ { 1909, 10, 18, 13, 2, -16 }, +/* '@' 0x40 */ { 1932, 22, 21, 24, 1, -16 }, +/* 'A' 0x41 */ { 1990, 14, 18, 16, 1, -16 }, +/* 'B' 0x42 */ { 2022, 13, 18, 16, 2, -16 }, +/* 'C' 0x43 */ { 2052, 15, 18, 17, 1, -16 }, +/* 'D' 0x44 */ { 2086, 14, 18, 17, 2, -16 }, +/* 'E' 0x45 */ { 2118, 12, 18, 15, 2, -16 }, +/* 'F' 0x46 */ { 2145, 11, 18, 14, 2, -16 }, +/* 'G' 0x47 */ { 2170, 16, 18, 18, 1, -16 }, +/* 'H' 0x48 */ { 2206, 13, 18, 17, 2, -16 }, +/* 'I' 0x49 */ { 2236, 2, 18, 7, 2, -16 }, +/* 'J' 0x4A */ { 2241, 9, 18, 13, 1, -16 }, +/* 'K' 0x4B */ { 2262, 13, 18, 16, 2, -16 }, +/* 'L' 0x4C */ { 2292, 10, 18, 14, 2, -16 }, +/* 'M' 0x4D */ { 2315, 16, 18, 20, 2, -16 }, +/* 'N' 0x4E */ { 2351, 13, 18, 18, 2, -16 }, +/* 'O' 0x4F */ { 2381, 17, 18, 19, 1, -16 }, +/* 'P' 0x50 */ { 2420, 12, 18, 16, 2, -16 }, +/* 'Q' 0x51 */ { 2447, 17, 19, 19, 1, -16 }, +/* 'R' 0x52 */ { 2488, 14, 18, 17, 2, -16 }, +/* 'S' 0x53 */ { 2520, 14, 18, 16, 1, -16 }, +/* 'T' 0x54 */ { 2552, 12, 18, 15, 1, -16 }, +/* 'U' 0x55 */ { 2579, 13, 18, 17, 2, -16 }, +/* 'V' 0x56 */ { 2609, 14, 18, 15, 1, -16 }, +/* 'W' 0x57 */ { 2641, 22, 18, 22, 0, -16 }, +/* 'X' 0x58 */ { 2691, 14, 18, 16, 1, -16 }, +/* 'Y' 0x59 */ { 2723, 14, 18, 16, 1, -16 }, +/* 'Z' 0x5A */ { 2755, 13, 18, 15, 1, -16 }, +/* '[' 0x5B */ { 2785, 4, 23, 7, 2, -16 }, +/* '\' 0x5C */ { 2797, 7, 18, 7, 0, -16 }, +/* ']' 0x5D */ { 2813, 4, 23, 7, 1, -16 }, +/* '^' 0x5E */ { 2825, 9, 9, 11, 1, -15 }, +/* '_' 0x5F */ { 2836, 15, 1, 13, -1, 5 }, +/* '`' 0x60 */ { 2838, 5, 4, 6, 1, -16 }, +/* 'a' 0x61 */ { 2841, 12, 13, 13, 1, -11 }, +/* 'b' 0x62 */ { 2861, 12, 18, 13, 1, -16 }, +/* 'c' 0x63 */ { 2888, 10, 13, 12, 1, -11 }, +/* 'd' 0x64 */ { 2905, 11, 18, 13, 1, -16 }, +/* 'e' 0x65 */ { 2930, 11, 13, 13, 1, -11 }, +/* 'f' 0x66 */ { 2948, 5, 18, 7, 1, -16 }, +/* 'g' 0x67 */ { 2960, 11, 18, 13, 1, -11 }, +/* 'h' 0x68 */ { 2985, 10, 18, 13, 1, -16 }, +/* 'i' 0x69 */ { 3008, 2, 18, 5, 2, -16 }, +/* 'j' 0x6A */ { 3013, 4, 23, 6, 0, -16 }, +/* 'k' 0x6B */ { 3025, 10, 18, 12, 1, -16 }, +/* 'l' 0x6C */ { 3048, 2, 18, 5, 1, -16 }, +/* 'm' 0x6D */ { 3053, 17, 13, 19, 1, -11 }, +/* 'n' 0x6E */ { 3081, 10, 13, 13, 1, -11 }, +/* 'o' 0x6F */ { 3098, 11, 13, 13, 1, -11 }, +/* 'p' 0x70 */ { 3116, 12, 17, 13, 1, -11 }, +/* 'q' 0x71 */ { 3142, 11, 17, 13, 1, -11 }, +/* 'r' 0x72 */ { 3166, 6, 13, 8, 1, -11 }, +/* 's' 0x73 */ { 3176, 10, 13, 12, 1, -11 }, +/* 't' 0x74 */ { 3193, 5, 16, 7, 1, -14 }, +/* 'u' 0x75 */ { 3203, 10, 13, 13, 1, -11 }, +/* 'v' 0x76 */ { 3220, 11, 13, 12, 0, -11 }, +/* 'w' 0x77 */ { 3238, 17, 13, 17, 0, -11 }, +/* 'x' 0x78 */ { 3266, 10, 13, 11, 1, -11 }, +/* 'y' 0x79 */ { 3283, 11, 18, 11, 0, -11 }, +/* 'z' 0x7A */ { 3308, 10, 13, 12, 1, -11 }, +/* '{' 0x7B */ { 3325, 5, 23, 8, 1, -16 }, +/* '|' 0x7C */ { 3340, 2, 23, 6, 2, -16 }, +/* '}' 0x7D */ { 3346, 5, 23, 8, 2, -16 }, +/* '~' 0x7E */ { 3361, 10, 5, 12, 1, -9 }, +/* 0x7F */ { 3368, 0, 0, 0, 0, 0 }, +/* 0x80 */ { 3368, 14, 17, 16, 1, -15 }, +/* 0x81 */ { 3398, 0, 0, 8, 0, 0 }, +/* 0x82 */ { 3398, 2, 5, 6, 2, 0 }, +/* 0x83 */ { 3400, 6, 23, 7, 0, -16 }, +/* 0x84 */ { 3418, 6, 5, 10, 2, 0 }, +/* 0x85 */ { 3422, 12, 2, 16, 2, 0 }, +/* 0x86 */ { 3425, 10, 21, 13, 2, -15 }, +/* 0x87 */ { 3452, 10, 20, 13, 2, -15 }, +/* 0x88 */ { 3477, 7, 4, 8, 0, -16 }, +/* 0x89 */ { 3481, 23, 18, 24, 0, -16 }, +/* 0x8A */ { 3533, 14, 21, 16, 1, -19 }, +/* 0x8B */ { 3570, 3, 8, 6, 1, -9 }, +/* 0x8C */ { 3573, 22, 18, 24, 1, -16 }, +/* 0x8D */ { 3623, 0, 0, 8, 0, 0 }, +/* 0x8E */ { 3623, 13, 21, 15, 1, -19 }, +/* 0x8F */ { 3658, 0, 0, 8, 0, 0 }, +/* 0x90 */ { 3658, 0, 0, 8, 0, 0 }, +/* 0x91 */ { 3658, 2, 6, 6, 2, -16 }, +/* 0x92 */ { 3660, 2, 6, 6, 2, -16 }, +/* 0x93 */ { 3662, 6, 6, 10, 2, -16 }, +/* 0x94 */ { 3667, 6, 6, 10, 2, -16 }, +/* 0x95 */ { 3672, 6, 6, 10, 2, -9 }, +/* 0x96 */ { 3677, 10, 2, 12, 1, -6 }, +/* 0x97 */ { 3680, 22, 2, 24, 1, -6 }, +/* 0x98 */ { 3686, 7, 3, 8, 0, -16 }, +/* 0x99 */ { 3689, 22, 13, 24, 2, -16 }, +/* 0x9A */ { 3725, 10, 18, 12, 1, -16 }, +/* 0x9B */ { 3748, 3, 8, 6, 2, -8 }, +/* 0x9C */ { 3751, 20, 13, 22, 1, -11 }, +/* 0x9D */ { 3784, 0, 0, 8, 0, 0 }, +/* 0x9E */ { 3784, 10, 18, 12, 1, -16 }, +/* 0x9F */ { 3807, 14, 21, 16, 1, -19 }, +/* 0xA0 */ { 3844, 0, 0, 7, 0, 0 }, +/* 0xA1 */ { 3844, 2, 18, 8, 3, -11 }, +/* 0xA2 */ { 3849, 11, 17, 13, 1, -13 }, +/* 0xA3 */ { 3873, 12, 18, 13, 0, -16 }, +/* 0xA4 */ { 3900, 9, 9, 13, 2, -11 }, +/* 0xA5 */ { 3911, 12, 17, 13, 1, -15 }, +/* 0xA6 */ { 3937, 2, 23, 6, 2, -16 }, +/* 0xA7 */ { 3943, 11, 23, 13, 1, -16 }, +/* 0xA8 */ { 3975, 6, 2, 8, 1, -15 }, +/* 0xA9 */ { 3977, 18, 17, 19, 1, -15 }, +/* 0xAA */ { 4016, 7, 11, 9, 1, -16 }, +/* 0xAB */ { 4026, 8, 8, 12, 2, -9 }, +/* 0xAC */ { 4034, 12, 6, 14, 1, -7 }, +/* 0xAD */ { 4043, 6, 2, 8, 1, -6 }, +/* 0xAE */ { 4045, 18, 17, 19, 1, -15 }, +/* 0xAF */ { 4084, 6, 2, 8, 1, -15 }, +/* 0xB0 */ { 4086, 7, 8, 15, 4, -15 }, +/* 0xB1 */ { 4093, 12, 15, 14, 1, -13 }, +/* 0xB2 */ { 4116, 7, 10, 8, 1, -17 }, +/* 0xB3 */ { 4125, 7, 10, 8, 1, -17 }, +/* 0xB4 */ { 4134, 5, 4, 8, 2, -16 }, +/* 0xB5 */ { 4137, 12, 17, 13, 2, -11 }, +/* 0xB6 */ { 4163, 11, 21, 13, 2, -16 }, +/* 0xB7 */ { 4192, 2, 2, 6, 2, -6 }, +/* 0xB8 */ { 4193, 6, 5, 8, 1, 2 }, +/* 0xB9 */ { 4197, 3, 10, 8, 3, -18 }, +/* 0xBA */ { 4201, 6, 11, 9, 1, -16 }, +/* 0xBB */ { 4210, 8, 8, 12, 2, -8 }, +/* 0xBC */ { 4218, 17, 17, 21, 3, -15 }, +/* 0xBD */ { 4255, 18, 18, 21, 3, -16 }, +/* 0xBE */ { 4296, 19, 18, 21, 1, -16 }, +/* 0xBF */ { 4339, 9, 18, 13, 3, -11 }, +/* 0xC0 */ { 4360, 8, 18, 6, -1, -18 }, +/* 0xC1 */ { 4378, 15, 17, 15, 0, -17 }, +/* 0xC2 */ { 4410, 13, 17, 16, 2, -17 }, +/* 0xC3 */ { 4438, 11, 17, 13, 2, -17 }, +/* 0xC4 */ { 4462, 16, 17, 16, -1, -17 }, +/* 0xC5 */ { 4496, 13, 17, 16, 2, -17 }, +/* 0xC6 */ { 4524, 14, 17, 15, 0, -17 }, +/* 0xC7 */ { 4554, 13, 17, 17, 2, -17 }, +/* 0xC8 */ { 4582, 17, 17, 19, 1, -17 }, +/* 0xC9 */ { 4619, 2, 17, 6, 2, -17 }, +/* 0xCA */ { 4624, 14, 17, 16, 2, -17 }, +/* 0xCB */ { 4654, 17, 17, 16, -1, -17 }, +/* 0xCC */ { 4691, 15, 17, 19, 2, -17 }, +/* 0xCD */ { 4723, 13, 17, 17, 2, -17 }, +/* 0xCE */ { 4751, 14, 17, 16, 1, -17 }, +/* 0xCF */ { 4781, 17, 17, 19, 1, -17 }, +/* 0xD0 */ { 4818, 13, 17, 17, 2, -17 }, +/* 0xD1 */ { 4846, 13, 17, 16, 2, -17 }, +/* 0xD2 */ { 4874, 0, 0, 5, 0, 0 }, +/* 0xD3 */ { 4874, 12, 17, 15, 2, -17 }, +/* 0xD4 */ { 4900, 14, 17, 14, 0, -17 }, +/* 0xD5 */ { 4930, 16, 17, 16, 0, -17 }, +/* 0xD6 */ { 4964, 16, 17, 18, 1, -17 }, +/* 0xD7 */ { 4998, 15, 17, 15, 0, -17 }, +/* 0xD8 */ { 5030, 16, 17, 19, 2, -17 }, +/* 0xD9 */ { 5064, 16, 17, 18, 1, -17 }, +/* 0xDA */ { 5098, 6, 20, 6, 0, -20 }, +/* 0xDB */ { 5113, 16, 20, 16, 0, -20 }, +/* 0xDC */ { 5153, 12, 17, 14, 1, -17 }, +/* 0xDD */ { 5179, 9, 17, 11, 1, -17 }, +/* 0xDE */ { 5199, 10, 22, 14, 2, -17 }, +/* 0xDF */ { 5227, 4, 17, 6, 1, -17 }, +/* 0xE0 */ { 5236, 10, 17, 14, 2, -17 }, +/* 0xE1 */ { 5258, 12, 13, 14, 1, -13 }, +/* 0xE2 */ { 5278, 11, 22, 14, 2, -17 }, +/* 0xE3 */ { 5309, 12, 18, 11, -1, -13 }, +/* 0xE4 */ { 5336, 11, 17, 13, 1, -17 }, +/* 0xE5 */ { 5360, 9, 13, 11, 1, -13 }, +/* 0xE6 */ { 5375, 9, 22, 11, 1, -17 }, +/* 0xE7 */ { 5400, 10, 18, 14, 2, -13 }, +/* 0xE8 */ { 5423, 11, 17, 13, 1, -17 }, +/* 0xE9 */ { 5447, 2, 13, 6, 2, -13 }, +/* 0xEA */ { 5451, 10, 13, 12, 2, -13 }, +/* 0xEB */ { 5468, 13, 17, 12, -1, -17 }, +/* 0xEC */ { 5496, 10, 18, 14, 2, -13 }, +/* 0xED */ { 5519, 11, 13, 11, 0, -13 }, +/* 0xEE */ { 5537, 9, 22, 11, 1, -17 }, +/* 0xEF */ { 5562, 11, 13, 13, 1, -13 }, +/* 0xF0 */ { 5580, 16, 13, 17, 0, -13 }, +/* 0xF1 */ { 5606, 11, 18, 14, 2, -13 }, +/* 0xF2 */ { 5631, 11, 18, 12, 1, -13 }, +/* 0xF3 */ { 5656, 13, 13, 15, 1, -13 }, +/* 0xF4 */ { 5678, 6, 13, 9, 1, -13 }, +/* 0xF5 */ { 5688, 10, 13, 14, 2, -13 }, +/* 0xF6 */ { 5705, 14, 18, 16, 1, -13 }, +/* 0xF7 */ { 5737, 13, 18, 13, 0, -13 }, +/* 0xF8 */ { 5767, 14, 18, 18, 2, -13 }, +/* 0xF9 */ { 5799, 16, 13, 18, 1, -13 }, +/* 0xFA */ { 5825, 6, 16, 6, 0, -16 }, +/* 0xFB */ { 5837, 10, 16, 14, 2, -16 }, +/* 0xFC */ { 5857, 11, 17, 13, 1, -17 }, +/* 0xFD */ { 5881, 10, 17, 14, 2, -17 }, +/* 0xFE */ { 5903, 16, 17, 18, 1, -17 }, +/* 0xFF */ { 5937, 0, 0, 5, 0, 0 }, +}; + +const GFXfont FreeSans12pt_Win1253 PROGMEM = { +(uint8_t*)FreeSans12pt_Win1253Bitmaps, +(GFXglyph*)FreeSans12pt_Win1253Glyphs, +0x01, 0xFF, 19 +}; diff --git a/src/graphics/niche/Fonts/FreeSans6pt_Win1253.h b/src/graphics/niche/Fonts/FreeSans6pt_Win1253.h new file mode 100644 index 000000000..440d136fa --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans6pt_Win1253.h @@ -0,0 +1,527 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans6pt_Win1253 +*/ +const uint8_t FreeSans6pt_Win1253Bitmaps[] PROGMEM = { +/* 0x01 */ 0x1C, 0x0A, 0x05, 0x04, 0xFE, 0x08, 0x1C, 0x02, 0x07, 0xE0, 0x9F, 0xC0, +/* 0x02 */ 0x3F, 0xF0, 0x40, 0xE0, 0x10, 0x3F, 0x04, 0x9E, 0x28, 0x14, 0x0E, 0x00, +/* 0x03 */ 0x3F, 0x10, 0x28, 0x06, 0x49, 0x80, 0x60, 0x19, 0x26, 0x31, 0x40, 0x8F, 0xC0, +/* 0x04 */ 0x3F, 0x10, 0x2A, 0x16, 0x49, 0xA1, 0x60, 0x19, 0xE6, 0x31, 0x40, 0x8F, 0xC0, +/* 0x05 */ 0x28, 0x15, 0x2A, 0xB5, 0x55, 0xA8, 0x54, 0x12, 0x04, 0x41, 0x08, 0x81, 0xC0, +/* 0x06 */ 0x04, 0x08, 0x88, 0x82, 0x07, 0x01, 0x11, 0xA2, 0xC4, 0x40, 0x70, 0x20, 0x88, 0x88, 0x10, 0x00, +/* 0x07 */ +/* 0x08 */ 0x03, 0x83, 0x44, 0x48, 0x28, 0x01, 0x80, 0x17, 0xFE, 0x08, 0x45, 0x28, 0x84, 0x00, +/* 0x09 */ 0x01, 0xC0, 0x68, 0x82, 0x41, 0x10, 0x02, 0x80, 0x06, 0x00, 0x14, 0x00, 0x8F, 0xFC, +/* 0x0A */ +/* 0x0B */ 0x22, 0x2A, 0xA2, 0x30, 0x18, 0x0A, 0x09, 0x04, 0x44, 0x14, 0x04, 0x00, +/* 0x0C */ 0x46, 0x00, 0x19, 0x03, 0x21, 0x20, 0x93, 0x04, 0x20, 0x11, 0x80, 0x50, 0x02, 0x7F, 0xE0, +/* 0x0D */ +/* 0x0E */ 0x08, 0x0E, 0x08, 0x88, 0x24, 0x12, 0x09, 0x05, 0x01, 0xFF, 0x8A, 0x02, 0x00, +/* 0x0F */ 0x3F, 0x14, 0xAA, 0x16, 0x01, 0x92, 0x60, 0x18, 0xC6, 0x49, 0x40, 0x8F, 0xC0, +/* 0x10 */ 0x1B, 0x02, 0xA0, 0x54, 0x12, 0x42, 0x48, 0x49, 0x31, 0x1E, 0x23, 0xEA, 0xFE, 0x3C, +/* 0x11 */ 0x3F, 0x02, 0x00, 0x20, 0x6D, 0x27, 0xF8, 0x3F, 0xC1, 0xFE, 0x37, 0xD0, 0xBE, 0x40, 0xE1, 0xE2, 0x00, +/* 0x12 */ 0x12, 0x42, 0x20, 0x24, 0xC0, 0x29, 0x99, 0x05, 0x23, 0x30, 0xB0, 0x30, 0x00, +/* 0x13 */ 0x3F, 0x88, 0x0A, 0x44, 0xD5, 0x58, 0x03, 0x00, 0x67, 0xCC, 0x71, 0x40, 0x47, 0xF0, +/* 0x14 */ 0x3F, 0x18, 0x69, 0x26, 0x85, 0xA1, 0x6C, 0xD8, 0x06, 0x31, 0x40, 0x8F, 0xC0, +/* 0x15 */ 0x3F, 0x11, 0x00, 0xE8, 0x03, 0xA0, 0x1F, 0xB3, 0x7E, 0x00, 0xE9, 0xE0, 0x23, 0x00, 0x40, 0x40, 0xFE, 0x00, +/* 0x16 */ 0x30, 0x38, 0x3A, 0x3E, 0x6E, 0xEB, 0xC3, 0xC3, 0x66, 0x3C, +/* 0x17 */ 0x3F, 0x04, 0x00, 0x82, 0x88, 0x5C, 0xA4, 0x49, 0x22, 0x81, 0x98, 0xC4, 0x40, 0xA3, 0xF0, +/* 0x18 */ 0x07, 0x80, 0x42, 0x04, 0x08, 0x21, 0x41, 0x42, 0x60, 0x0E, 0x8C, 0xB2, 0x89, 0x50, 0x52, 0x82, 0x80, +/* 0x19 */ 0x3F, 0xC4, 0x02, 0x80, 0x18, 0x01, 0xB3, 0x1B, 0xB9, 0x80, 0x19, 0xE1, 0x40, 0x23, 0xFC, +/* 0x1A */ 0xFF, 0xC0, 0x67, 0x34, 0x58, 0x4C, 0x46, 0x03, 0x11, 0x80, 0xFF, 0xC0, +/* 0x1B */ 0x0F, 0xC0, 0x40, 0x82, 0x49, 0x08, 0x04, 0x00, 0x00, 0x12, 0x02, 0x31, 0x34, 0x0B, 0x88, 0x45, 0x00, 0x20, +/* 0x1C */ 0x3F, 0x88, 0x0A, 0x44, 0xC9, 0x19, 0x3B, 0x00, 0x60, 0x4C, 0x71, 0x40, 0x47, 0xF0, +/* 0x1D */ 0x3F, 0x8B, 0x0A, 0x00, 0xC8, 0x18, 0x13, 0x00, 0x48, 0xCA, 0xC1, 0x44, 0x53, 0x30, +/* 0x1E */ 0x19, 0xC2, 0x02, 0x50, 0x1E, 0x49, 0x80, 0x12, 0x01, 0x27, 0x92, 0x01, 0x10, 0x20, 0xFC, +/* 0x1F */ 0x30, 0x1C, 0x0C, 0x3E, 0x7E, 0xCF, 0x07, 0xC7, 0x7F, 0x3F, +/* ' ' 0x20 */ +/* '!' 0x21 */ 0xFC, 0x80, +/* '"' 0x22 */ 0xB6, 0x80, +/* '#' 0x23 */ 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, +/* '$' 0x24 */ 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00, +/* '%' 0x25 */ 0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, +/* '&' 0x26 */ 0x71, 0x24, 0x9C, 0x62, 0x58, 0xA7, 0xF4, +/* ''' 0x27 */ 0xE0, +/* '(' 0x28 */ 0x5A, 0xAA, 0x94, +/* ')' 0x29 */ 0x89, 0x12, 0x49, 0x29, 0x00, +/* '*' 0x2A */ 0x5E, 0x80, +/* '+' 0x2B */ 0x21, 0x3E, 0x42, 0x00, +/* ',' 0x2C */ 0xE0, +/* '-' 0x2D */ 0xC0, +/* '.' 0x2E */ 0x80, +/* '/' 0x2F */ 0x24, 0xA4, 0xA4, 0x80, +/* '0' 0x30 */ 0x76, 0xE3, 0x18, 0xC6, 0x3B, 0x70, +/* '1' 0x31 */ 0x27, 0x92, 0x49, 0x20, +/* '2' 0x32 */ 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, +/* '3' 0x33 */ 0x79, 0x30, 0x43, 0x18, 0x10, 0x71, 0x78, +/* '4' 0x34 */ 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, +/* '5' 0x35 */ 0xFC, 0x21, 0xE8, 0x84, 0x31, 0xF0, +/* '6' 0x36 */ 0x74, 0x61, 0xE8, 0xC6, 0x31, 0x70, +/* '7' 0x37 */ 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, +/* '8' 0x38 */ 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, +/* '9' 0x39 */ 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53, 0x78, +/* ':' 0x3A */ 0x82, +/* ';' 0x3B */ 0x87, +/* '<' 0x3C */ 0x3E, 0x30, 0x60, 0x80, +/* '=' 0x3D */ 0xF8, 0x3E, +/* '>' 0x3E */ 0xE0, 0xC6, 0xC8, 0x00, +/* '?' 0x3F */ 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, +/* '@' 0x40 */ 0x0F, 0x86, 0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, +/* 'A' 0x41 */ 0x18, 0x18, 0x24, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, +/* 'B' 0x42 */ 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, +/* 'C' 0x43 */ 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, +/* 'D' 0x44 */ 0xF9, 0x0A, 0x1C, 0x18, 0x30, 0x61, 0xC2, 0xF8, +/* 'E' 0x45 */ 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, +/* 'F' 0x46 */ 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, +/* 'G' 0x47 */ 0x1E, 0x61, 0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, +/* 'H' 0x48 */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, +/* 'I' 0x49 */ 0xFF, 0x80, +/* 'J' 0x4A */ 0x08, 0x42, 0x10, 0x87, 0x29, 0x70, +/* 'K' 0x4B */ 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, +/* 'L' 0x4C */ 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, +/* 'M' 0x4D */ 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, +/* 'N' 0x4E */ 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, +/* 'O' 0x4F */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1E, 0x00, +/* 'P' 0x50 */ 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, +/* 'Q' 0x51 */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x16, 0xC6, 0x1F, 0x00, 0x40, +/* 'R' 0x52 */ 0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, +/* 'S' 0x53 */ 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, +/* 'T' 0x54 */ 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, +/* 'U' 0x55 */ 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, +/* 'V' 0x56 */ 0xC2, 0x85, 0x0B, 0x22, 0x44, 0x8E, 0x0C, 0x18, +/* 'W' 0x57 */ 0xC4, 0x28, 0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, +/* 'X' 0x58 */ 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xA3, 0x84, +/* 'Y' 0x59 */ 0xC3, 0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, +/* 'Z' 0x5A */ 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, +/* '[' 0x5B */ 0xEA, 0xAA, 0xAB, +/* '\' 0x5C */ 0x92, 0x24, 0x89, 0x20, +/* ']' 0x5D */ 0xD5, 0x55, 0x57, +/* '^' 0x5E */ 0x46, 0xA9, +/* '_' 0x5F */ 0xFE, +/* '`' 0x60 */ 0x80, +/* 'a' 0x61 */ 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, +/* 'b' 0x62 */ 0x84, 0x3D, 0x18, 0xC6, 0x31, 0xF0, +/* 'c' 0x63 */ 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, +/* 'd' 0x64 */ 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, +/* 'e' 0x65 */ 0x39, 0x38, 0x7F, 0x81, 0x13, 0x80, +/* 'f' 0x66 */ 0x6B, 0xA4, 0x92, 0x40, +/* 'g' 0x67 */ 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, +/* 'h' 0x68 */ 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, +/* 'i' 0x69 */ 0xBF, 0x80, +/* 'j' 0x6A */ 0x45, 0x55, 0x57, +/* 'k' 0x6B */ 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, +/* 'l' 0x6C */ 0xFF, 0x80, +/* 'm' 0x6D */ 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, +/* 'n' 0x6E */ 0xF4, 0x63, 0x18, 0xC6, 0x20, +/* 'o' 0x6F */ 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, +/* 'p' 0x70 */ 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, +/* 'q' 0x71 */ 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04, +/* 'r' 0x72 */ 0xF2, 0x49, 0x20, +/* 's' 0x73 */ 0x7A, 0x50, 0xE0, 0xE5, 0xE0, +/* 't' 0x74 */ 0x5D, 0x24, 0x93, +/* 'u' 0x75 */ 0x8C, 0x63, 0x18, 0xCF, 0xA0, +/* 'v' 0x76 */ 0x85, 0x24, 0x92, 0x30, 0xC3, 0x00, +/* 'w' 0x77 */ 0x89, 0x59, 0x59, 0x55, 0x56, 0x26, 0x26, +/* 'x' 0x78 */ 0x4A, 0x4C, 0x43, 0x27, 0x20, +/* 'y' 0x79 */ 0x8A, 0x52, 0xA5, 0x18, 0x84, 0x22, 0x00, +/* 'z' 0x7A */ 0x78, 0x44, 0x46, 0x23, 0xE0, +/* '{' 0x7B */ 0x6A, 0xAA, 0xA9, +/* '|' 0x7C */ 0xFF, 0xE0, +/* '}' 0x7D */ 0x95, 0x55, 0x56, +/* '~' 0x7E */ 0x66, 0x60, +/* 0x7F */ +/* 0x80 */ 0x1C, 0x45, 0x07, 0xE4, 0x1F, 0x10, 0x10, 0x1E, +/* 0x81 */ +/* 0x82 */ 0xE0, +/* 0x83 */ 0x6B, 0xA4, 0x92, 0x49, 0x60, +/* 0x84 */ 0xB6, 0x80, +/* 0x85 */ 0xA8, +/* 0x86 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0x21, 0x08, +/* 0x87 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0xF9, 0x08, +/* 0x88 */ 0x54, +/* 0x89 */ 0x62, 0x09, 0x40, 0x98, 0x06, 0x80, 0x10, 0x01, 0x66, 0x29, 0x92, 0x99, 0x06, 0x60, +/* 0x8A */ 0x28, 0x47, 0xA1, 0x83, 0x07, 0x83, 0x87, 0x17, 0x80, +/* 0x8B */ 0x64, +/* 0x8C */ 0x3B, 0xE8, 0xC2, 0x08, 0x41, 0x08, 0x3F, 0x04, 0x20, 0x82, 0x30, 0x3B, 0xE0, +/* 0x8D */ +/* 0x8E */ 0x14, 0x11, 0xF8, 0x30, 0xC1, 0x04, 0x18, 0x61, 0xFC, +/* 0x8F */ +/* 0x90 */ +/* 0x91 */ 0xE0, +/* 0x92 */ 0xE0, +/* 0x93 */ 0xB6, 0x80, +/* 0x94 */ 0xB6, 0x80, +/* 0x95 */ 0xFF, 0x80, +/* 0x96 */ 0xFC, +/* 0x97 */ 0xFF, 0xF0, +/* 0x98 */ 0xDB, +/* 0x99 */ 0xE6, 0x28, 0xCD, 0x19, 0xA3, 0x34, 0x6A, 0x8B, 0x51, 0x68, +/* 0x9A */ 0x52, 0x69, 0x8E, 0x19, 0x60, +/* 0x9B */ 0x98, +/* 0x9C */ 0x7B, 0xD9, 0xCE, 0x10, 0xC3, 0xF8, 0x41, 0x9C, 0x5E, 0xF0, +/* 0x9D */ +/* 0x9E */ 0x51, 0x1E, 0x11, 0x11, 0x88, 0xF8, +/* 0x9F */ 0x29, 0x05, 0x12, 0x22, 0x87, 0x04, 0x08, 0x10, 0x20, +/* 0xA0 */ +/* 0xA1 */ 0xBF, 0x80, +/* 0xA2 */ 0x23, 0xAB, 0x4A, 0x52, 0xAE, 0x20, +/* 0xA3 */ 0x39, 0x14, 0x10, 0xF0, 0x82, 0x1C, 0x4C, +/* 0xA4 */ 0xFC, 0x63, 0xF0, +/* 0xA5 */ 0x8C, 0x54, 0xAF, 0x93, 0xE4, 0x20, +/* 0xA6 */ 0xF9, 0xF0, +/* 0xA7 */ 0x32, 0x91, 0xC9, 0x47, 0x26, 0x14, 0xA4, 0xC0, +/* 0xA8 */ 0xA0, +/* 0xA9 */ 0x3E, 0x3F, 0xB8, 0xF4, 0x1A, 0x0D, 0x17, 0x76, 0xC6, 0x3E, 0x00, +/* 0xAA */ 0x61, 0x79, 0x60, +/* 0xAB */ 0x5A, 0xA5, +/* 0xAC */ 0xFC, 0x10, 0x40, +/* 0xAD */ +/* 0xAE */ 0x3E, 0x31, 0xB7, 0x72, 0x99, 0xCC, 0xC7, 0x56, 0xC6, 0x3E, 0x00, +/* 0xAF */ 0xE0, +/* 0xB0 */ 0x69, 0x96, +/* 0xB1 */ 0x21, 0x3E, 0x42, 0x03, 0xE0, +/* 0xB2 */ 0x69, 0x3C, 0xF0, +/* 0xB3 */ 0x79, 0x29, 0x70, +/* 0xB4 */ 0x80, +/* 0xB5 */ 0x8A, 0x28, 0xA2, 0x8A, 0x6E, 0xE0, 0x80, +/* 0xB6 */ 0x7F, 0xAE, 0xBA, 0x68, 0xA2, 0x8A, 0x28, 0xA0, +/* 0xB7 */ 0x80, +/* 0xB8 */ 0x67, 0x80, +/* 0xB9 */ 0x75, 0x50, +/* 0xBA */ 0x69, 0x96, 0xF0, +/* 0xBB */ 0xA5, 0x5A, +/* 0xBC */ 0x42, 0x30, 0x84, 0x41, 0x10, 0x48, 0x82, 0x61, 0x28, 0x8F, 0x20, 0x80, +/* 0xBD */ 0x40, 0x63, 0x11, 0x09, 0x74, 0xA8, 0x84, 0x44, 0x44, 0x43, 0x80, +/* 0xBE */ 0x71, 0x24, 0x82, 0x20, 0x50, 0x98, 0x9A, 0x61, 0x28, 0x4F, 0x20, 0x80, +/* 0xBF */ 0x20, 0x08, 0x44, 0x42, 0x11, 0x70, +/* 0xC0 */ 0x2D, 0x02, 0x22, 0x22, 0x22, +/* 0xC1 */ 0x10, 0x50, 0xA1, 0x44, 0x4F, 0x91, 0x41, 0x82, +/* 0xC2 */ 0xFA, 0x18, 0x61, 0xFE, 0x18, 0x61, 0xF8, +/* 0xC3 */ 0xFE, 0x08, 0x20, 0x82, 0x08, 0x20, 0x80, +/* 0xC4 */ 0x08, 0x0A, 0x05, 0x02, 0x82, 0x21, 0x11, 0x04, 0x82, 0x7F, 0x00, +/* 0xC5 */ 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, +/* 0xC6 */ 0x7E, 0x08, 0x20, 0x41, 0x04, 0x08, 0x20, 0xFE, +/* 0xC7 */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, +/* 0xC8 */ 0x38, 0x8A, 0x0C, 0x1B, 0xB0, 0x60, 0xA2, 0x38, +/* 0xC9 */ 0xFF, 0x80, +/* 0xCA */ 0x83, 0x0A, 0x24, 0x8A, 0x1A, 0x22, 0x42, 0x82, +/* 0xCB */ 0x08, 0x0A, 0x05, 0x02, 0x82, 0x21, 0x11, 0x04, 0x82, 0x41, 0x00, +/* 0xCC */ 0x83, 0x8F, 0x1D, 0x5A, 0xB5, 0x6A, 0xC9, 0x92, +/* 0xCD */ 0x83, 0x86, 0x8D, 0x19, 0x31, 0x62, 0xC3, 0x82, +/* 0xCE */ 0xFC, 0x00, 0x00, 0x78, 0x00, 0x00, 0xFC, +/* 0xCF */ 0x38, 0x8A, 0x0C, 0x18, 0x30, 0x60, 0xA2, 0x38, +/* 0xD0 */ 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x82, +/* 0xD1 */ 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, +/* 0xD2 */ +/* 0xD3 */ 0xFE, 0x04, 0x08, 0x10, 0x84, 0x20, 0xFC, +/* 0xD4 */ 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, +/* 0xD5 */ 0x82, 0x89, 0x11, 0x41, 0x02, 0x04, 0x08, 0x10, +/* 0xD6 */ 0x10, 0xFA, 0x4C, 0x99, 0x32, 0x64, 0xBE, 0x10, +/* 0xD7 */ 0x82, 0x89, 0x11, 0x41, 0x05, 0x11, 0x22, 0x82, +/* 0xD8 */ 0x93, 0x26, 0x4C, 0x99, 0x2F, 0x84, 0x08, 0x10, +/* 0xD9 */ 0x38, 0x8A, 0x0C, 0x18, 0x30, 0x60, 0xA2, 0xEE, +/* 0xDA */ 0xA1, 0x24, 0x92, 0x49, 0x00, +/* 0xDB */ 0x28, 0x02, 0x0A, 0x24, 0x45, 0x04, 0x08, 0x10, 0x20, 0x40, +/* 0xDC */ 0x11, 0x00, 0xD9, 0x4A, 0x52, 0x93, 0x40, +/* 0xDD */ 0x11, 0x00, 0xF8, 0x41, 0x90, 0x83, 0xC0, +/* 0xDE */ 0x11, 0x01, 0x6C, 0xC6, 0x31, 0x8C, 0x42, 0x10, +/* 0xDF */ 0x62, 0xAA, 0xA0, +/* 0xE0 */ 0x25, 0x81, 0x18, 0xC6, 0x31, 0x8B, 0x80, +/* 0xE1 */ 0x6C, 0xA5, 0x29, 0x49, 0xA0, +/* 0xE2 */ 0x74, 0x63, 0x1B, 0x46, 0x39, 0xB4, 0x20, +/* 0xE3 */ 0x44, 0x89, 0x11, 0x42, 0x85, 0x04, 0x08, 0x10, +/* 0xE4 */ 0x71, 0x1D, 0x18, 0xC6, 0x31, 0x70, +/* 0xE5 */ 0x7C, 0x20, 0xC8, 0x41, 0xE0, +/* 0xE6 */ 0x72, 0x44, 0x88, 0x88, 0x71, 0x20, +/* 0xE7 */ 0xB6, 0x63, 0x18, 0xC6, 0x21, 0x08, +/* 0xE8 */ 0x74, 0x63, 0x1F, 0xC6, 0x31, 0x70, +/* 0xE9 */ 0xFE, +/* 0xEA */ 0x8A, 0x4A, 0x38, 0x92, 0x48, 0x80, +/* 0xEB */ 0x20, 0x41, 0x04, 0x28, 0xA2, 0x91, 0x44, +/* 0xEC */ 0x8C, 0x63, 0x18, 0xC7, 0xF0, 0x80, +/* 0xED */ 0x8C, 0x54, 0xA5, 0x10, 0x80, +/* 0xEE */ 0x68, 0x86, 0x48, 0x88, 0x71, 0x20, +/* 0xEF */ 0x74, 0x63, 0x18, 0xC5, 0xC0, +/* 0xF0 */ 0xFF, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, +/* 0xF1 */ 0x74, 0x63, 0x18, 0xC7, 0xD0, 0x80, +/* 0xF2 */ 0x34, 0x88, 0x88, 0x71, 0x60, +/* 0xF3 */ 0x7F, 0x12, 0x24, 0x48, 0x91, 0x1C, 0x00, +/* 0xF4 */ 0xE9, 0x24, 0x90, +/* 0xF5 */ 0x8C, 0x63, 0x18, 0xC5, 0xC0, +/* 0xF6 */ 0x5A, 0x59, 0x65, 0x95, 0x53, 0x84, 0x10, +/* 0xF7 */ 0x49, 0x24, 0x8C, 0x30, 0xC4, 0x92, 0x48, +/* 0xF8 */ 0x93, 0x26, 0x4C, 0x99, 0x32, 0x5F, 0x08, 0x10, +/* 0xF9 */ 0x45, 0x06, 0x4C, 0x99, 0x32, 0x5B, 0x00, +/* 0xFA */ 0xA1, 0x24, 0x92, 0x40, +/* 0xFB */ 0x50, 0x23, 0x18, 0xC6, 0x31, 0x70, +/* 0xFC */ 0x11, 0x00, 0xE8, 0xC6, 0x31, 0x8B, 0x80, +/* 0xFD */ 0x21, 0x01, 0x18, 0xC6, 0x31, 0x8B, 0x80, +/* 0xFE */ 0x08, 0x20, 0x02, 0x28, 0x32, 0x64, 0xC9, 0x92, 0xD8, +/* 0xFF */ +}; + +const GFXglyph FreeSans6pt_Win1253Glyphs[] PROGMEM = { +/* 0x01 */ { 0, 9, 10, 11, 1, -9 }, +/* 0x02 */ { 12, 9, 10, 11, 1, -8 }, +/* 0x03 */ { 24, 10, 10, 12, 1, -8 }, +/* 0x04 */ { 37, 10, 10, 12, 1, -8 }, +/* 0x05 */ { 50, 10, 10, 12, 1, -9 }, +/* 0x06 */ { 63, 11, 11, 13, 1, -9 }, +/* 0x07 */ { 79, 0, 0, 8, 0, 0 }, +/* 0x08 */ { 79, 12, 9, 14, 1, -8 }, +/* 0x09 */ { 93, 14, 8, 16, 1, -7 }, +/* 0x0A */ { 107, 0, 0, 8, 0, 0 }, +/* 0x0B */ { 107, 9, 10, 11, 1, -9 }, +/* 0x0C */ { 119, 13, 9, 15, 1, -8 }, +/* 0x0D */ { 134, 0, 0, 8, 0, 0 }, +/* 0x0E */ { 134, 9, 11, 11, 1, -9 }, +/* 0x0F */ { 147, 10, 10, 12, 1, -9 }, +/* 0x10 */ { 160, 11, 10, 13, 1, -9 }, +/* 0x11 */ { 174, 13, 10, 15, 1, -9 }, +/* 0x12 */ { 191, 10, 10, 12, 1, -9 }, +/* 0x13 */ { 204, 11, 10, 13, 1, -9 }, +/* 0x14 */ { 218, 10, 10, 12, 1, -9 }, +/* 0x15 */ { 231, 14, 10, 16, 1, -9 }, +/* 0x16 */ { 249, 8, 10, 10, 1, -9 }, +/* 0x17 */ { 259, 12, 10, 14, 1, -9 }, +/* 0x18 */ { 274, 13, 10, 15, 1, -9 }, +/* 0x19 */ { 291, 12, 10, 14, 1, -9 }, +/* 0x1A */ { 306, 9, 10, 11, 1, -8 }, +/* 0x1B */ { 318, 14, 10, 16, 1, -9 }, +/* 0x1C */ { 336, 11, 10, 13, 1, -9 }, +/* 0x1D */ { 350, 11, 10, 13, 1, -9 }, +/* 0x1E */ { 364, 12, 10, 14, 1, -9 }, +/* 0x1F */ { 379, 8, 10, 11, 2, -9 }, +/* ' ' 0x20 */ { 389, 0, 0, 3, 0, 0 }, +/* '!' 0x21 */ { 389, 1, 9, 4, 2, -8 }, +/* '"' 0x22 */ { 391, 3, 3, 4, 0, -8 }, +/* '#' 0x23 */ { 393, 7, 8, 7, 0, -7 }, +/* '$' 0x24 */ { 400, 6, 11, 7, 0, -9 }, +/* '%' 0x25 */ { 409, 10, 9, 11, 0, -8 }, +/* '&' 0x26 */ { 421, 6, 9, 8, 1, -8 }, +/* ''' 0x27 */ { 428, 1, 3, 2, 1, -8 }, +/* '(' 0x28 */ { 429, 2, 11, 4, 1, -8 }, +/* ')' 0x29 */ { 432, 3, 11, 4, 0, -8 }, +/* '*' 0x2A */ { 437, 3, 3, 5, 1, -8 }, +/* '+' 0x2B */ { 439, 5, 5, 7, 1, -4 }, +/* ',' 0x2C */ { 443, 1, 3, 3, 1, 0 }, +/* '-' 0x2D */ { 444, 2, 1, 4, 1, -3 }, +/* '.' 0x2E */ { 445, 1, 1, 3, 1, 0 }, +/* '/' 0x2F */ { 446, 3, 9, 3, 0, -8 }, +/* '0' 0x30 */ { 450, 5, 9, 7, 1, -8 }, +/* '1' 0x31 */ { 456, 3, 9, 7, 1, -8 }, +/* '2' 0x32 */ { 460, 6, 9, 7, 0, -8 }, +/* '3' 0x33 */ { 467, 6, 9, 7, 0, -8 }, +/* '4' 0x34 */ { 474, 6, 9, 7, 0, -8 }, +/* '5' 0x35 */ { 481, 5, 9, 7, 1, -8 }, +/* '6' 0x36 */ { 487, 5, 9, 7, 1, -8 }, +/* '7' 0x37 */ { 493, 5, 9, 7, 1, -8 }, +/* '8' 0x38 */ { 499, 6, 9, 7, 0, -8 }, +/* '9' 0x39 */ { 506, 6, 9, 7, 0, -8 }, +/* ':' 0x3A */ { 513, 1, 7, 3, 1, -6 }, +/* ';' 0x3B */ { 514, 1, 8, 3, 1, -5 }, +/* '<' 0x3C */ { 515, 5, 5, 7, 1, -4 }, +/* '=' 0x3D */ { 519, 5, 3, 7, 1, -3 }, +/* '>' 0x3E */ { 521, 5, 5, 7, 1, -4 }, +/* '?' 0x3F */ { 525, 5, 9, 7, 1, -8 }, +/* '@' 0x40 */ { 531, 11, 11, 12, 0, -8 }, +/* 'A' 0x41 */ { 547, 8, 9, 8, 0, -8 }, +/* 'B' 0x42 */ { 556, 6, 9, 8, 1, -8 }, +/* 'C' 0x43 */ { 563, 8, 9, 9, 0, -8 }, +/* 'D' 0x44 */ { 572, 7, 9, 8, 1, -8 }, +/* 'E' 0x45 */ { 580, 6, 9, 8, 1, -8 }, +/* 'F' 0x46 */ { 587, 6, 9, 7, 1, -8 }, +/* 'G' 0x47 */ { 594, 8, 9, 9, 0, -8 }, +/* 'H' 0x48 */ { 603, 7, 9, 9, 1, -8 }, +/* 'I' 0x49 */ { 611, 1, 9, 3, 1, -8 }, +/* 'J' 0x4A */ { 613, 5, 9, 6, 0, -8 }, +/* 'K' 0x4B */ { 619, 7, 9, 8, 1, -8 }, +/* 'L' 0x4C */ { 627, 5, 9, 7, 1, -8 }, +/* 'M' 0x4D */ { 633, 8, 9, 10, 1, -8 }, +/* 'N' 0x4E */ { 642, 7, 9, 9, 1, -8 }, +/* 'O' 0x4F */ { 650, 9, 9, 9, 0, -8 }, +/* 'P' 0x50 */ { 661, 6, 9, 8, 1, -8 }, +/* 'Q' 0x51 */ { 668, 9, 10, 9, 0, -8 }, +/* 'R' 0x52 */ { 680, 7, 9, 9, 1, -8 }, +/* 'S' 0x53 */ { 688, 6, 9, 8, 1, -8 }, +/* 'T' 0x54 */ { 695, 7, 9, 8, 0, -8 }, +/* 'U' 0x55 */ { 703, 7, 9, 9, 1, -8 }, +/* 'V' 0x56 */ { 711, 7, 9, 8, 0, -8 }, +/* 'W' 0x57 */ { 719, 11, 9, 11, 0, -8 }, +/* 'X' 0x58 */ { 732, 6, 9, 8, 1, -8 }, +/* 'Y' 0x59 */ { 739, 8, 9, 8, 0, -8 }, +/* 'Z' 0x5A */ { 748, 7, 9, 7, 0, -8 }, +/* '[' 0x5B */ { 756, 2, 12, 3, 1, -8 }, +/* '\' 0x5C */ { 759, 3, 9, 3, 0, -8 }, +/* ']' 0x5D */ { 763, 2, 12, 3, 0, -8 }, +/* '^' 0x5E */ { 766, 4, 4, 6, 1, -8 }, +/* '_' 0x5F */ { 768, 7, 1, 7, 0, 2 }, +/* '`' 0x60 */ { 769, 1, 1, 3, 1, -8 }, +/* 'a' 0x61 */ { 770, 6, 7, 7, 0, -6 }, +/* 'b' 0x62 */ { 776, 5, 9, 7, 1, -8 }, +/* 'c' 0x63 */ { 782, 6, 7, 6, 0, -6 }, +/* 'd' 0x64 */ { 788, 6, 9, 7, 0, -8 }, +/* 'e' 0x65 */ { 795, 6, 7, 6, 0, -6 }, +/* 'f' 0x66 */ { 801, 3, 9, 3, 0, -8 }, +/* 'g' 0x67 */ { 805, 6, 10, 7, 0, -6 }, +/* 'h' 0x68 */ { 813, 5, 9, 6, 1, -8 }, +/* 'i' 0x69 */ { 819, 1, 9, 3, 1, -8 }, +/* 'j' 0x6A */ { 821, 2, 12, 3, 0, -8 }, +/* 'k' 0x6B */ { 824, 5, 9, 6, 1, -8 }, +/* 'l' 0x6C */ { 830, 1, 9, 3, 1, -8 }, +/* 'm' 0x6D */ { 832, 8, 7, 10, 1, -6 }, +/* 'n' 0x6E */ { 839, 5, 7, 6, 1, -6 }, +/* 'o' 0x6F */ { 844, 6, 7, 6, 0, -6 }, +/* 'p' 0x70 */ { 850, 5, 9, 7, 1, -6 }, +/* 'q' 0x71 */ { 856, 6, 9, 7, 0, -6 }, +/* 'r' 0x72 */ { 863, 3, 7, 4, 1, -6 }, +/* 's' 0x73 */ { 866, 5, 7, 6, 0, -6 }, +/* 't' 0x74 */ { 871, 3, 8, 3, 0, -7 }, +/* 'u' 0x75 */ { 874, 5, 7, 6, 1, -6 }, +/* 'v' 0x76 */ { 879, 6, 7, 6, 0, -6 }, +/* 'w' 0x77 */ { 885, 8, 7, 9, 0, -6 }, +/* 'x' 0x78 */ { 892, 5, 7, 6, 0, -6 }, +/* 'y' 0x79 */ { 897, 5, 10, 6, 0, -6 }, +/* 'z' 0x7A */ { 904, 5, 7, 6, 0, -6 }, +/* '{' 0x7B */ { 909, 2, 12, 4, 1, -8 }, +/* '|' 0x7C */ { 912, 1, 11, 3, 1, -8 }, +/* '}' 0x7D */ { 914, 2, 12, 4, 1, -8 }, +/* '~' 0x7E */ { 917, 6, 2, 6, 0, -4 }, +/* 0x7F */ { 919, 0, 0, 0, 0, 0 }, +/* 0x80 */ { 919, 7, 9, 8, 0, -8 }, +/* 0x81 */ { 927, 0, 0, 8, 0, 0 }, +/* 0x82 */ { 927, 1, 3, 3, 1, 0 }, +/* 0x83 */ { 928, 3, 12, 3, 0, -8 }, +/* 0x84 */ { 933, 3, 3, 5, 1, 0 }, +/* 0x85 */ { 935, 5, 1, 7, 1, 0 }, +/* 0x86 */ { 936, 5, 11, 7, 1, -8 }, +/* 0x87 */ { 943, 5, 11, 7, 1, -8 }, +/* 0x88 */ { 950, 3, 2, 4, 0, -9 }, +/* 0x89 */ { 951, 12, 9, 12, 0, -8 }, +/* 0x8A */ { 965, 6, 11, 8, 1, -9 }, +/* 0x8B */ { 974, 2, 3, 4, 1, -4 }, +/* 0x8C */ { 975, 11, 9, 12, 0, -8 }, +/* 0x8D */ { 988, 0, 0, 8, 0, 0 }, +/* 0x8E */ { 988, 7, 10, 7, 0, -9 }, +/* 0x8F */ { 997, 0, 0, 8, 0, 0 }, +/* 0x90 */ { 997, 0, 0, 8, 0, 0 }, +/* 0x91 */ { 997, 1, 3, 3, 1, -8 }, +/* 0x92 */ { 998, 1, 3, 2, 1, -8 }, +/* 0x93 */ { 999, 3, 3, 5, 1, -8 }, +/* 0x94 */ { 1001, 3, 3, 5, 1, -8 }, +/* 0x95 */ { 1003, 3, 3, 5, 1, -5 }, +/* 0x96 */ { 1005, 6, 1, 6, 0, -3 }, +/* 0x97 */ { 1006, 12, 1, 12, 0, -3 }, +/* 0x98 */ { 1008, 4, 2, 4, 0, -8 }, +/* 0x99 */ { 1009, 11, 7, 12, 1, -8 }, +/* 0x9A */ { 1019, 4, 9, 6, 1, -8 }, +/* 0x9B */ { 1024, 2, 3, 3, 1, -4 }, +/* 0x9C */ { 1025, 11, 7, 11, 0, -6 }, +/* 0x9D */ { 1035, 0, 0, 8, 0, 0 }, +/* 0x9E */ { 1035, 5, 9, 6, 0, -8 }, +/* 0x9F */ { 1041, 7, 10, 8, 1, -9 }, +/* 0xA0 */ { 1050, 0, 0, 3, 0, 0 }, +/* 0xA1 */ { 1050, 1, 9, 4, 1, -5 }, +/* 0xA2 */ { 1052, 5, 9, 7, 1, -7 }, +/* 0xA3 */ { 1058, 6, 9, 7, 0, -8 }, +/* 0xA4 */ { 1065, 5, 4, 7, 1, -5 }, +/* 0xA5 */ { 1068, 5, 9, 7, 1, -8 }, +/* 0xA6 */ { 1074, 1, 12, 3, 1, -8 }, +/* 0xA7 */ { 1076, 5, 12, 7, 1, -8 }, +/* 0xA8 */ { 1084, 3, 1, 4, 0, -7 }, +/* 0xA9 */ { 1085, 9, 9, 10, 0, -8 }, +/* 0xAA */ { 1096, 4, 5, 4, 0, -8 }, +/* 0xAB */ { 1099, 4, 4, 6, 1, -4 }, +/* 0xAC */ { 1101, 6, 3, 7, 1, -4 }, +/* 0xAD */ { 1104, 0, 0, 0, 0, 0 }, +/* 0xAE */ { 1104, 9, 9, 10, 0, -8 }, +/* 0xAF */ { 1115, 3, 1, 4, 0, -8 }, +/* 0xB0 */ { 1116, 4, 4, 7, 2, -8 }, +/* 0xB1 */ { 1118, 5, 7, 7, 1, -6 }, +/* 0xB2 */ { 1123, 4, 5, 4, 0, -9 }, +/* 0xB3 */ { 1126, 4, 5, 4, 0, -9 }, +/* 0xB4 */ { 1129, 1, 1, 4, 1, -8 }, +/* 0xB5 */ { 1130, 6, 9, 7, 1, -6 }, +/* 0xB6 */ { 1137, 6, 10, 6, 1, -8 }, +/* 0xB7 */ { 1145, 1, 1, 3, 1, -2 }, +/* 0xB8 */ { 1146, 3, 3, 4, 1, 1 }, +/* 0xB9 */ { 1148, 2, 6, 4, 1, -9 }, +/* 0xBA */ { 1150, 4, 5, 4, 0, -8 }, +/* 0xBB */ { 1153, 4, 4, 6, 1, -5 }, +/* 0xBC */ { 1155, 10, 9, 10, 1, -8 }, +/* 0xBD */ { 1167, 9, 9, 10, 1, -8 }, +/* 0xBE */ { 1178, 10, 9, 11, 0, -8 }, +/* 0xBF */ { 1190, 5, 9, 7, 1, -5 }, +/* 0xC0 */ { 1196, 4, 10, 3, -1, -10 }, +/* 0xC1 */ { 1201, 7, 9, 7, 0, -9 }, +/* 0xC2 */ { 1209, 6, 9, 8, 1, -9 }, +/* 0xC3 */ { 1216, 6, 9, 7, 1, -9 }, +/* 0xC4 */ { 1223, 9, 9, 7, -1, -9 }, +/* 0xC5 */ { 1234, 6, 9, 8, 1, -9 }, +/* 0xC6 */ { 1241, 7, 9, 7, 0, -9 }, +/* 0xC7 */ { 1249, 7, 9, 9, 1, -9 }, +/* 0xC8 */ { 1257, 7, 9, 9, 1, -9 }, +/* 0xC9 */ { 1265, 1, 9, 3, 1, -9 }, +/* 0xCA */ { 1267, 7, 9, 8, 1, -9 }, +/* 0xCB */ { 1275, 9, 9, 7, -1, -9 }, +/* 0xCC */ { 1286, 7, 9, 9, 1, -9 }, +/* 0xCD */ { 1294, 7, 9, 9, 1, -9 }, +/* 0xCE */ { 1302, 6, 9, 8, 1, -9 }, +/* 0xCF */ { 1309, 7, 9, 9, 1, -9 }, +/* 0xD0 */ { 1317, 7, 9, 9, 1, -9 }, +/* 0xD1 */ { 1325, 6, 9, 8, 1, -9 }, +/* 0xD2 */ { 1332, 0, 0, 5, 0, 0 }, +/* 0xD3 */ { 1332, 6, 9, 7, 1, -9 }, +/* 0xD4 */ { 1339, 7, 9, 7, 0, -9 }, +/* 0xD5 */ { 1347, 7, 9, 7, 0, -9 }, +/* 0xD6 */ { 1355, 7, 9, 9, 1, -9 }, +/* 0xD7 */ { 1363, 7, 9, 7, 0, -9 }, +/* 0xD8 */ { 1371, 7, 9, 9, 1, -9 }, +/* 0xD9 */ { 1379, 7, 9, 9, 1, -9 }, +/* 0xDA */ { 1387, 3, 11, 3, 0, -11 }, +/* 0xDB */ { 1392, 7, 11, 7, 0, -11 }, +/* 0xDC */ { 1402, 5, 10, 7, 1, -10 }, +/* 0xDD */ { 1409, 5, 10, 5, 0, -10 }, +/* 0xDE */ { 1416, 5, 12, 7, 1, -10 }, +/* 0xDF */ { 1424, 2, 10, 3, 1, -10 }, +/* 0xE0 */ { 1427, 5, 10, 7, 1, -10 }, +/* 0xE1 */ { 1434, 5, 7, 7, 1, -7 }, +/* 0xE2 */ { 1439, 5, 11, 7, 1, -9 }, +/* 0xE3 */ { 1446, 7, 9, 5, -1, -7 }, +/* 0xE4 */ { 1454, 5, 9, 7, 1, -9 }, +/* 0xE5 */ { 1460, 5, 7, 5, 0, -7 }, +/* 0xE6 */ { 1465, 4, 11, 5, 1, -9 }, +/* 0xE7 */ { 1471, 5, 9, 7, 1, -7 }, +/* 0xE8 */ { 1477, 5, 9, 7, 1, -9 }, +/* 0xE9 */ { 1483, 1, 7, 3, 1, -7 }, +/* 0xEA */ { 1484, 6, 7, 7, 1, -7 }, +/* 0xEB */ { 1490, 6, 9, 5, -1, -9 }, +/* 0xEC */ { 1497, 5, 9, 7, 1, -7 }, +/* 0xED */ { 1503, 5, 7, 5, 0, -7 }, +/* 0xEE */ { 1508, 4, 11, 5, 1, -9 }, +/* 0xEF */ { 1514, 5, 7, 7, 1, -7 }, +/* 0xF0 */ { 1519, 8, 7, 8, 0, -7 }, +/* 0xF1 */ { 1526, 5, 9, 7, 1, -7 }, +/* 0xF2 */ { 1532, 4, 9, 6, 1, -7 }, +/* 0xF3 */ { 1537, 7, 7, 7, 1, -7 }, +/* 0xF4 */ { 1544, 3, 7, 5, 1, -7 }, +/* 0xF5 */ { 1547, 5, 7, 7, 1, -7 }, +/* 0xF6 */ { 1552, 6, 9, 8, 1, -7 }, +/* 0xF7 */ { 1559, 6, 9, 6, 0, -7 }, +/* 0xF8 */ { 1566, 7, 9, 9, 1, -7 }, +/* 0xF9 */ { 1574, 7, 7, 9, 1, -7 }, +/* 0xFA */ { 1581, 3, 9, 3, 0, -9 }, +/* 0xFB */ { 1585, 5, 9, 7, 1, -9 }, +/* 0xFC */ { 1591, 5, 10, 7, 1, -10 }, +/* 0xFD */ { 1598, 5, 10, 7, 1, -10 }, +/* 0xFE */ { 1605, 7, 10, 9, 1, -10 }, +/* 0xFF */ { 1614, 0, 0, 5, 0, 0 }, +}; + +const GFXfont FreeSans6pt_Win1253 PROGMEM = { +(uint8_t*)FreeSans6pt_Win1253Bitmaps, +(GFXglyph*)FreeSans6pt_Win1253Glyphs, +0x01, 0xFF, 10 +}; diff --git a/src/graphics/niche/Fonts/FreeSans9pt_Win1253.h b/src/graphics/niche/Fonts/FreeSans9pt_Win1253.h new file mode 100644 index 000000000..e9ff547cb --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans9pt_Win1253.h @@ -0,0 +1,527 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans9pt_Win1253 +*/ +const uint8_t FreeSans9pt_Win1253Bitmaps[] PROGMEM = { +/* 0x01 */ 0x07, 0x00, 0x0A, 0x00, 0x24, 0x00, 0x48, 0x01, 0x10, 0x04, 0x40, 0x10, 0xFF, 0x20, 0x02, 0x81, 0xFD, 0x00, 0x06, 0x07, 0xF4, 0x08, 0x24, 0x0F, 0x88, 0x11, 0x0F, 0xDC, 0x00, +/* 0x02 */ 0x3F, 0x70, 0x81, 0x11, 0x03, 0xE4, 0x08, 0x28, 0x1F, 0xD0, 0x00, 0x60, 0x7F, 0x20, 0x02, 0x43, 0xFC, 0x44, 0x00, 0x44, 0x00, 0x48, 0x00, 0x90, 0x00, 0xA0, 0x01, 0xC0, 0x00, +/* 0x03 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x8C, 0x63, 0x18, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x20, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x04 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x30, 0x88, 0x62, 0x08, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x05 */ 0x0B, 0x10, 0x14, 0xA8, 0x12, 0x50, 0x29, 0x42, 0x24, 0xA5, 0x32, 0x95, 0x5A, 0x09, 0x48, 0x09, 0x24, 0x01, 0x10, 0x01, 0x48, 0x02, 0xA4, 0x02, 0x42, 0x04, 0x01, 0x98, 0x00, 0x60, +/* 0x06 */ 0x00, 0x80, 0x22, 0x80, 0x65, 0x00, 0xBE, 0xE1, 0x82, 0x4E, 0x03, 0x24, 0x04, 0x28, 0x06, 0x30, 0x12, 0x20, 0x3C, 0xA0, 0xC3, 0xFE, 0x80, 0x4D, 0x00, 0xA6, 0x01, 0x80, 0x00, +/* 0x07 */ +/* 0x08 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x02, 0x00, 0x03, 0x01, 0x00, 0x09, 0x88, 0x0C, 0x0C, +/* 0x09 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x00, +/* 0x0A */ +/* 0x0B */ 0x1C, 0x1C, 0x31, 0xB1, 0x90, 0x50, 0x50, 0x10, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, 0x02, 0x80, 0x02, 0x40, 0x01, 0x10, 0x01, 0x04, 0x01, 0x01, 0x01, 0x00, 0x41, 0x00, 0x11, 0x00, 0x07, 0x00, 0x01, 0x00, +/* 0x0C */ 0x06, 0x00, 0x0A, 0x00, 0x12, 0x00, 0x32, 0x01, 0x84, 0x04, 0x10, 0x08, 0x98, 0x1C, 0x18, 0x40, 0x48, 0x82, 0x11, 0xF0, 0x74, 0x02, 0x18, 0x70, 0x2F, 0x9F, 0x80, +/* 0x0D */ +/* 0x0E */ 0x01, 0x00, 0x05, 0x00, 0x0A, 0x00, 0x3E, 0x00, 0x82, 0x02, 0x82, 0x06, 0x04, 0x10, 0x04, 0x20, 0x08, 0x40, 0x10, 0xFF, 0x22, 0x00, 0x29, 0xFF, 0x3F, 0x8F, 0xDF, 0x9F, 0x01, 0xC0, +/* 0x0F */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x36, 0x03, 0x60, 0x00, 0xCC, 0x19, 0xA4, 0x4B, 0x00, 0x06, 0x8E, 0x2B, 0x22, 0x66, 0x7C, 0xCC, 0x71, 0x98, 0x03, 0x00, +/* 0x10 */ 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x54, 0x00, 0xA8, 0x01, 0x50, 0x02, 0xA0, 0x05, 0x20, 0x32, 0x61, 0xC4, 0x74, 0x49, 0x10, 0x6C, 0x00, 0xD8, 0x01, 0x10, 0x00, +/* 0x11 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x40, 0x29, 0x00, 0x31, 0x84, 0x63, 0x18, 0xC0, 0x00, 0x80, 0x15, 0x03, 0x7E, 0x02, 0xFA, 0x04, 0xE4, 0x18, 0x84, 0x00, 0x06, 0x0C, 0x03, 0xE0, +/* 0x12 */ 0x02, 0x08, 0x01, 0x08, 0x40, 0x10, 0xC0, 0x08, 0xC0, 0x60, 0x80, 0x28, 0x04, 0x12, 0x4C, 0x10, 0x80, 0x08, 0x23, 0x0E, 0x08, 0xC4, 0x82, 0x04, 0x20, 0x83, 0x09, 0x82, 0x47, 0x01, 0x1C, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x00, +/* 0x13 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x08, 0x65, 0x28, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x14 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x22, 0x29, 0x83, 0x30, 0x00, 0x65, 0x14, 0xD3, 0x4D, 0xBA, 0xEB, 0x38, 0xE6, 0x00, 0x0A, 0x00, 0x24, 0x38, 0x44, 0x01, 0x07, 0x1C, 0x01, 0xC0, +/* 0x15 */ 0x07, 0xC0, 0x30, 0x18, 0x80, 0x32, 0x00, 0xF8, 0x01, 0xF1, 0x09, 0xA5, 0x28, 0x40, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x16 */ 0x0C, 0x00, 0xC0, 0x1C, 0x03, 0x80, 0xF8, 0xBB, 0x36, 0xC7, 0x99, 0xF3, 0xFE, 0x3F, 0xC3, 0xF0, 0x7E, 0x0E, 0xC1, 0x8E, 0xE0, 0x20, +/* 0x17 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x10, 0x01, 0x20, 0x1D, 0x44, 0x42, 0x84, 0x85, 0x00, 0x86, 0x00, 0xC4, 0x00, 0x44, 0x7C, 0x44, 0x00, 0x06, 0x0C, 0x03, 0xE0, +/* 0x18 */ 0x01, 0xE0, 0x00, 0x84, 0x00, 0x40, 0x80, 0x20, 0x10, 0x08, 0x24, 0x02, 0x41, 0x00, 0x86, 0x03, 0x12, 0x03, 0xB4, 0x03, 0x52, 0x81, 0x23, 0x80, 0x70, 0xA0, 0x14, 0x28, 0x05, 0x0A, 0x01, 0x42, 0x80, 0x50, +/* 0x19 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x33, 0x18, 0x60, 0x00, 0xDC, 0xE1, 0xB9, 0xC3, 0x7B, 0xC6, 0x63, 0x0A, 0x00, 0x24, 0xF0, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x1A */ 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x82, 0x0C, 0x10, 0x60, 0x03, 0x04, 0x18, 0x00, 0xFF, 0xFC, +/* 0x1B */ 0x07, 0xF0, 0x06, 0x0C, 0x04, 0x01, 0x04, 0x00, 0x44, 0x22, 0x12, 0x2A, 0x89, 0x00, 0x04, 0x80, 0x02, 0x44, 0x11, 0x01, 0xF0, 0x04, 0x01, 0x0D, 0x01, 0x6A, 0x41, 0x2C, 0x00, 0x05, 0xC0, 0x0E, 0x18, 0x18, +/* 0x1C */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0xC0, 0x2A, 0x00, 0x33, 0x00, 0x66, 0x00, 0xCC, 0x39, 0x80, 0x83, 0x00, 0x06, 0x00, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x1D */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x70, 0x28, 0x00, 0x31, 0x80, 0x63, 0x18, 0xC0, 0x31, 0x80, 0x03, 0x00, 0x06, 0x60, 0x0D, 0x33, 0x12, 0x10, 0x48, 0x21, 0x23, 0x8C, 0x00, +/* 0x1E */ 0x03, 0x00, 0x07, 0x9E, 0x07, 0x00, 0x86, 0x00, 0x27, 0xC0, 0x0F, 0xC0, 0x07, 0x8C, 0x62, 0x06, 0x31, 0x20, 0x00, 0x90, 0x00, 0x48, 0x00, 0x24, 0x3E, 0x11, 0x00, 0x10, 0x40, 0x10, 0x18, 0x30, 0x03, 0xE0, +/* 0x1F */ 0x18, 0x02, 0x80, 0x4C, 0x16, 0x41, 0x24, 0x3C, 0x88, 0x6E, 0x65, 0xF2, 0x78, 0x46, 0x88, 0xCF, 0x18, 0x02, 0x80, 0x8C, 0x60, 0x70, +/* ' ' 0x20 */ +/* '!' 0x21 */ 0xFF, 0xFF, 0xF0, 0xC0, +/* '"' 0x22 */ 0xDE, 0xF7, 0x20, +/* '#' 0x23 */ 0x09, 0x86, 0x41, 0x91, 0xFF, 0x13, 0x04, 0xC3, 0x20, 0xC8, 0xFF, 0x89, 0x82, 0x61, 0x90, +/* '$' 0x24 */ 0x10, 0x1F, 0x14, 0xDA, 0x3D, 0x1E, 0x83, 0x40, 0x78, 0x17, 0x08, 0xF4, 0x7A, 0x35, 0x33, 0xF0, 0x40, 0x20, +/* '%' 0x25 */ 0x38, 0x10, 0xEC, 0x20, 0xC6, 0x20, 0xC6, 0x40, 0xC6, 0x40, 0x6C, 0x80, 0x39, 0x00, 0x01, 0x3C, 0x02, 0x77, 0x02, 0x63, 0x04, 0x63, 0x04, 0x77, 0x08, 0x3C, +/* '&' 0x26 */ 0x0E, 0x0C, 0xC3, 0x30, 0xCC, 0x1E, 0x03, 0x03, 0xC1, 0x9B, 0xC2, 0xF0, 0xEC, 0x19, 0x8F, 0x3C, 0x40, +/* ''' 0x27 */ 0xFE, +/* '(' 0x28 */ 0x13, 0x26, 0x6C, 0xCC, 0xCC, 0xC4, 0x66, 0x23, 0x10, +/* ')' 0x29 */ 0x8C, 0x46, 0x63, 0x33, 0x33, 0x32, 0x66, 0x4C, 0x80, +/* '*' 0x2A */ 0x25, 0x7E, 0xA5, 0x00, +/* '+' 0x2B */ 0x30, 0xC3, 0x3F, 0x30, 0xC3, 0x0C, +/* ',' 0x2C */ 0xD6, +/* '-' 0x2D */ 0xF0, +/* '.' 0x2E */ 0xC0, +/* '/' 0x2F */ 0x08, 0x44, 0x21, 0x10, 0x84, 0x42, 0x11, 0x08, 0x00, +/* '0' 0x30 */ 0x3C, 0x66, 0x42, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x42, 0x66, 0x3C, +/* '1' 0x31 */ 0x11, 0x3F, 0x33, 0x33, 0x33, 0x33, 0x30, +/* '2' 0x32 */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x1C, 0x1C, 0x1C, 0x18, 0x18, 0x10, 0x08, 0x07, 0xF8, +/* '3' 0x33 */ 0x3C, 0x66, 0xC3, 0xC3, 0x03, 0x06, 0x1C, 0x07, 0x03, 0xC3, 0xC3, 0x66, 0x3C, +/* '4' 0x34 */ 0x0C, 0x18, 0x71, 0x62, 0xC9, 0xA3, 0x46, 0xFE, 0x18, 0x30, 0x60, 0xC0, +/* '5' 0x35 */ 0x7F, 0x20, 0x10, 0x08, 0x08, 0x07, 0xF3, 0x8C, 0x03, 0x01, 0x80, 0xF0, 0x6C, 0x63, 0xE0, +/* '6' 0x36 */ 0x1E, 0x31, 0x98, 0x78, 0x0C, 0x06, 0xF3, 0x8D, 0x83, 0xC1, 0xE0, 0xD0, 0x6C, 0x63, 0xE0, +/* '7' 0x37 */ 0xFF, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x18, 0x18, 0x18, 0x10, 0x30, 0x30, +/* '8' 0x38 */ 0x3E, 0x31, 0xB0, 0x78, 0x3C, 0x1B, 0x18, 0xF8, 0xC6, 0xC1, 0xE0, 0xF0, 0x6C, 0x63, 0xE0, +/* '9' 0x39 */ 0x3C, 0x66, 0xC2, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC2, 0x66, 0x3C, +/* ':' 0x3A */ 0xC0, 0x00, 0x30, +/* ';' 0x3B */ 0xC0, 0x00, 0x00, 0x64, 0xA0, +/* '<' 0x3C */ 0x00, 0x81, 0xC7, 0x8E, 0x0C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0x80, +/* '=' 0x3D */ 0xFF, 0x80, 0x00, 0x1F, 0xF0, +/* '>' 0x3E */ 0xE0, 0x1C, 0x03, 0x80, 0x30, 0x70, 0xE3, 0x81, 0x00, +/* '?' 0x3F */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x18, 0x38, 0x18, 0x18, 0x0C, 0x00, 0x00, 0x01, 0x80, +/* '@' 0x40 */ 0x03, 0xF0, 0x06, 0x0E, 0x06, 0x01, 0x86, 0x00, 0x66, 0x1D, 0xBB, 0x31, 0xCF, 0x18, 0xC7, 0x98, 0x63, 0xCC, 0x31, 0xE6, 0x11, 0xB3, 0x99, 0xCC, 0xF7, 0x86, 0x00, 0x01, 0x80, 0x00, 0x70, 0x40, 0x0F, 0xE0, +/* 'A' 0x41 */ 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, +/* 'B' 0x42 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, +/* 'C' 0x43 */ 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, +/* 'D' 0x44 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x03, 0x60, 0xCF, 0xF0, +/* 'E' 0x45 */ 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, +/* 'F' 0x46 */ 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, +/* 'G' 0x47 */ 0x0F, 0x83, 0x0E, 0x60, 0x66, 0x03, 0xC0, 0x0C, 0x00, 0xC1, 0xFC, 0x03, 0xC0, 0x36, 0x03, 0x60, 0x73, 0x0F, 0x0F, 0x10, +/* 'H' 0x48 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, +/* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xC0, +/* 'J' 0x4A */ 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1E, 0x27, 0x80, +/* 'K' 0x4B */ 0xC0, 0xF0, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0x98, 0xC3, 0x30, 0xCC, 0x1B, 0x03, 0xC0, 0xC0, +/* 'L' 0x4C */ 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, +/* 'M' 0x4D */ 0xE0, 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, 0x80, +/* 'N' 0x4E */ 0xE0, 0x7C, 0x0F, 0xC1, 0xE8, 0x3D, 0x87, 0x98, 0xF1, 0x1E, 0x33, 0xC3, 0x78, 0x6F, 0x07, 0xE0, 0x7C, 0x0E, +/* 'O' 0x4F */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, 0x00, +/* 'P' 0x50 */ 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, +/* 'Q' 0x51 */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x6C, 0x60, 0xC0, 0xFB, 0x00, 0x08, +/* 'R' 0x52 */ 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x70, +/* 'S' 0x53 */ 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, +/* 'T' 0x54 */ 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, +/* 'U' 0x55 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xB0, 0x61, 0xF0, +/* 'V' 0x56 */ 0xC0, 0x6C, 0x0D, 0x81, 0x10, 0x63, 0x0C, 0x61, 0x04, 0x60, 0xCC, 0x19, 0x01, 0x60, 0x3C, 0x07, 0x00, 0x60, +/* 'W' 0x57 */ 0xC1, 0x81, 0x61, 0xC3, 0x61, 0xC3, 0x61, 0x43, 0x62, 0x62, 0x22, 0x66, 0x32, 0x26, 0x36, 0x26, 0x14, 0x34, 0x14, 0x34, 0x1C, 0x1C, 0x18, 0x1C, 0x08, 0x18, +/* 'X' 0x58 */ 0xC0, 0xD8, 0x66, 0x18, 0xCC, 0x1E, 0x07, 0x00, 0xC0, 0x78, 0x32, 0x0C, 0xC6, 0x1B, 0x07, 0xC0, 0xC0, +/* 'Y' 0x59 */ 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, +/* 'Z' 0x5A */ 0xFF, 0xC0, 0x60, 0x30, 0x0C, 0x06, 0x03, 0x01, 0xC0, 0x60, 0x30, 0x18, 0x06, 0x03, 0x00, 0xFF, 0xC0, +/* '[' 0x5B */ 0xFB, 0x6D, 0xB6, 0xDB, 0x6D, 0xB6, 0xE0, +/* '\' 0x5C */ 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x80, +/* ']' 0x5D */ 0xED, 0xB6, 0xDB, 0x6D, 0xB6, 0xDB, 0xE0, +/* '^' 0x5E */ 0x30, 0x60, 0xA2, 0x44, 0xD8, 0xA1, 0x80, +/* '_' 0x5F */ 0xFF, 0xC0, +/* '`' 0x60 */ 0xC6, 0x30, +/* 'a' 0x61 */ 0x7E, 0x71, 0xB0, 0xC0, 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, +/* 'b' 0x62 */ 0xC0, 0x60, 0x30, 0x1B, 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, +/* 'c' 0x63 */ 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 'd' 0x64 */ 0x03, 0x03, 0x03, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, +/* 'e' 0x65 */ 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 'f' 0x66 */ 0x36, 0x6F, 0x66, 0x66, 0x66, 0x66, 0x60, +/* 'g' 0x67 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC6, 0x7C, +/* 'h' 0x68 */ 0xC0, 0xC0, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, +/* 'i' 0x69 */ 0xC3, 0xFF, 0xFF, 0xC0, +/* 'j' 0x6A */ 0x30, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, +/* 'k' 0x6B */ 0xC0, 0xC0, 0xC0, 0xC2, 0xC4, 0xCC, 0xD8, 0xF8, 0xEC, 0xC4, 0xC6, 0xC3, 0xC3, +/* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xC0, +/* 'm' 0x6D */ 0xDE, 0xF7, 0x1C, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xC0, +/* 'n' 0x6E */ 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, +/* 'o' 0x6F */ 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 'p' 0x70 */ 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, +/* 'q' 0x71 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0x03, +/* 'r' 0x72 */ 0xDF, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, +/* 's' 0x73 */ 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, +/* 't' 0x74 */ 0x66, 0xF6, 0x66, 0x66, 0x66, 0x67, +/* 'u' 0x75 */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, +/* 'v' 0x76 */ 0xC1, 0xA0, 0x98, 0xCC, 0x42, 0x21, 0xB0, 0xD0, 0x28, 0x1C, 0x0C, 0x00, +/* 'w' 0x77 */ 0xC6, 0x1E, 0x38, 0x91, 0xC4, 0xCA, 0x66, 0xD3, 0x16, 0xD0, 0xA6, 0x87, 0x1C, 0x38, 0xC0, 0xC6, 0x00, +/* 'x' 0x78 */ 0x87, 0x89, 0xB1, 0xC3, 0x07, 0x1E, 0x26, 0xC5, 0x0C, +/* 'y' 0x79 */ 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, +/* 'z' 0x7A */ 0xFE, 0x0C, 0x30, 0xC1, 0x86, 0x18, 0x20, 0xC1, 0xFC, +/* '{' 0x7B */ 0x36, 0x66, 0x66, 0x6E, 0xCE, 0x66, 0x66, 0x66, 0x30, +/* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, +/* '}' 0x7D */ 0xC6, 0x66, 0x66, 0x67, 0x37, 0x66, 0x66, 0x66, 0xC0, +/* '~' 0x7E */ 0x61, 0x24, 0x38, +/* 0x7F */ +/* 0x80 */ 0x07, 0xC6, 0x13, 0x00, 0xC0, 0x60, 0x3F, 0xE6, 0x03, 0xFC, 0x60, 0x0C, 0x03, 0x00, 0x61, 0x07, 0xC0, +/* 0x81 */ +/* 0x82 */ 0xDC, +/* 0x83 */ 0x19, 0x8C, 0xF3, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0xE0, +/* 0x84 */ 0xDA, 0x76, +/* 0x85 */ 0xCC, 0xC0, +/* 0x86 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, +/* 0x87 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, +/* 0x88 */ 0x72, 0xA2, +/* 0x89 */ 0x70, 0x80, 0x22, 0x20, 0x08, 0x90, 0x02, 0x24, 0x00, 0x72, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x10, 0x00, 0x09, 0xC7, 0x84, 0x8B, 0x31, 0x22, 0x84, 0x88, 0xB3, 0x21, 0xC7, 0x80, +/* 0x8A */ 0x1B, 0x03, 0x80, 0x00, 0xFC, 0x61, 0xB0, 0x3C, 0x0F, 0x00, 0x78, 0x07, 0xC0, 0x38, 0x03, 0xC0, 0xF0, 0x36, 0x18, 0xFC, +/* 0x8B */ 0x69, +/* 0x8C */ 0x1E, 0xFE, 0x43, 0x81, 0x83, 0x06, 0x06, 0x0C, 0x0C, 0x18, 0x18, 0x30, 0x3F, 0xE0, 0x60, 0xC0, 0xC1, 0x81, 0x81, 0x83, 0x01, 0x8E, 0x01, 0xEF, 0xE0, +/* 0x8D */ +/* 0x8E */ 0x1B, 0x03, 0x80, 0x03, 0xFF, 0x01, 0x80, 0xC0, 0x30, 0x18, 0x0C, 0x07, 0x01, 0x80, 0xC0, 0x60, 0x18, 0x0C, 0x03, 0xFF, +/* 0x8F */ +/* 0x90 */ +/* 0x91 */ 0x6B, +/* 0x92 */ 0xD6, +/* 0x93 */ 0x4C, 0xA5, 0xB0, +/* 0x94 */ 0xDA, 0x53, 0x20, +/* 0x95 */ 0x6F, 0xFF, 0x60, +/* 0x96 */ 0xFE, +/* 0x97 */ 0xFF, 0xFF, +/* 0x98 */ 0x4D, 0xC0, +/* 0x99 */ 0xFC, 0xE1, 0xCC, 0x38, 0x73, 0x0E, 0x1C, 0xC3, 0x8F, 0x30, 0xD2, 0xCC, 0x34, 0xB3, 0x0D, 0x6C, 0xC3, 0x53, 0x30, 0xCC, 0xCC, 0x33, 0x30, +/* 0x9A */ 0x24, 0x3C, 0x18, 0x7E, 0xE3, 0xC0, 0xC0, 0x60, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, +/* 0x9B */ 0x96, +/* 0x9C */ 0x3C, 0xF8, 0xCF, 0x1B, 0x0C, 0x1E, 0x18, 0x3C, 0x3F, 0xF8, 0x60, 0x30, 0xC0, 0x61, 0x83, 0x67, 0x8C, 0x79, 0xF0, +/* 0x9D */ +/* 0x9E */ 0x48, 0xF0, 0xC7, 0xF0, 0x61, 0x86, 0x0C, 0x30, 0xC1, 0x06, 0x0F, 0xE0, +/* 0x9F */ 0x19, 0x80, 0x00, 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, +/* 0xA0 */ +/* 0xA1 */ 0xCF, 0xFF, 0xFF, 0xC0, +/* 0xA2 */ 0x08, 0x04, 0x0F, 0x8D, 0x6C, 0x9E, 0x43, 0x21, 0x90, 0xC8, 0x64, 0xDA, 0xC7, 0xC0, 0x80, 0x40, +/* 0xA3 */ 0x1F, 0x0C, 0x66, 0x0D, 0x83, 0x60, 0x0C, 0x0F, 0xC0, 0x60, 0x18, 0x06, 0x03, 0x01, 0xF1, 0x43, 0xC0, +/* 0xA4 */ 0xFF, 0xDF, 0x1E, 0x3E, 0xFF, 0xC0, +/* 0xA5 */ 0xC3, 0x42, 0x42, 0x24, 0x24, 0x3C, 0x18, 0x7E, 0x18, 0x7E, 0x18, 0x18, 0x18, +/* 0xA6 */ 0xFF, 0xFC, 0x0F, 0xFF, 0xC0, +/* 0xA7 */ 0x0C, 0x09, 0x0C, 0xC6, 0x63, 0x81, 0xE3, 0x19, 0x87, 0xE1, 0xB8, 0xC6, 0x41, 0xC0, 0x73, 0x19, 0x8C, 0x66, 0x1E, 0x00, +/* 0xA8 */ 0xCC, +/* 0xA9 */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9B, 0xC6, 0xD9, 0x8F, 0x60, 0x3D, 0x00, 0xF4, 0x03, 0xD8, 0x0D, 0xE6, 0x67, 0xF3, 0x86, 0x18, 0x0F, 0xC0, +/* 0xAA */ 0x74, 0x8D, 0xA9, 0x7C, 0x1F, +/* 0xAB */ 0x22, 0xCF, 0x26, 0x46, 0x64, 0x40, +/* 0xAC */ 0xFF, 0x80, 0xC0, 0x60, 0x30, 0x18, +/* 0xAD */ +/* 0xAE */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9F, 0xE6, 0xD0, 0x8F, 0x42, 0x3D, 0xF0, 0xF4, 0x23, 0xD0, 0x8D, 0xC2, 0x67, 0x0B, 0x86, 0x18, 0x0F, 0xC0, +/* 0xAF */ 0xF8, +/* 0xB0 */ 0x74, 0x63, 0x17, 0x00, +/* 0xB1 */ 0x0C, 0x06, 0x03, 0x07, 0xE0, 0xC0, 0x60, 0x30, 0x18, 0x00, 0x00, 0x3F, 0xE0, +/* 0xB2 */ 0x7B, 0x30, 0xC3, 0x11, 0x84, 0x3F, +/* 0xB3 */ 0x7D, 0x8C, 0x18, 0xC0, 0x60, 0xF1, 0xBE, +/* 0xB4 */ 0x36, 0xC0, +/* 0xB5 */ 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x1B, 0x0D, 0x86, 0xE7, 0x7D, 0xF0, 0x18, 0x0C, 0x00, +/* 0xB6 */ 0x3F, 0x7E, 0xF2, 0xF2, 0xF2, 0xF2, 0xF2, 0x72, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, +/* 0xB7 */ 0xE0, +/* 0xB8 */ 0x21, 0xC7, 0xE0, +/* 0xB9 */ 0x3D, 0xB6, 0xD8, +/* 0xBA */ 0x74, 0x63, 0x18, 0xB8, 0x1F, +/* 0xBB */ 0x89, 0x98, 0x99, 0x3C, 0xD1, 0x00, +/* 0xBC */ 0x20, 0x43, 0x81, 0x06, 0x08, 0x18, 0x20, 0x61, 0x01, 0x84, 0x06, 0x21, 0x80, 0x86, 0x04, 0x78, 0x32, 0x60, 0x87, 0xC4, 0x06, 0x10, 0x18, +/* 0xBD */ 0x20, 0x43, 0x81, 0x06, 0x08, 0x18, 0x20, 0x61, 0x01, 0x8D, 0xE6, 0x2C, 0xC1, 0x03, 0x0C, 0x0C, 0x20, 0x41, 0x86, 0x0C, 0x30, 0x20, 0xFC, +/* 0xBE */ 0x78, 0x11, 0x98, 0x40, 0x31, 0x00, 0x82, 0x00, 0xC8, 0x01, 0x90, 0x33, 0x43, 0x3D, 0x06, 0x02, 0x3C, 0x08, 0x98, 0x10, 0xF8, 0x40, 0x61, 0x00, 0xC0, +/* 0xBF */ 0x0C, 0x00, 0x00, 0x01, 0x80, 0xC0, 0xC0, 0xE0, 0xC0, 0xC0, 0x60, 0xF0, 0x6C, 0x63, 0xE0, +/* 0xC0 */ 0x0C, 0xDB, 0xD3, 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, +/* 0xC1 */ 0x0E, 0x01, 0xC0, 0x6C, 0x0D, 0x81, 0xB0, 0x63, 0x0C, 0x61, 0xFC, 0x7F, 0xCC, 0x19, 0x83, 0x60, 0x3C, 0x06, +/* 0xC2 */ 0xFF, 0x3F, 0xEC, 0x0F, 0x03, 0xC0, 0xFF, 0xEF, 0xFB, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0xFE, 0xFF, 0x00, +/* 0xC3 */ 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, +/* 0xC4 */ 0x07, 0x00, 0x38, 0x01, 0xC0, 0x1B, 0x00, 0xD8, 0x0C, 0x60, 0x63, 0x03, 0x18, 0x30, 0x61, 0x83, 0x18, 0x0C, 0xFF, 0xE7, 0xFF, 0x00, +/* 0xC5 */ 0xFF, 0xFF, 0xFC, 0x03, 0x00, 0xC0, 0x3F, 0xEF, 0xFB, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0xFF, 0xFF, 0xC0, +/* 0xC6 */ 0x7F, 0xDF, 0xF0, 0x18, 0x0C, 0x07, 0x01, 0x80, 0xC0, 0x60, 0x38, 0x0C, 0x06, 0x03, 0xFF, 0xFF, 0xC0, +/* 0xC7 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0xFF, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, +/* 0xC8 */ 0x0F, 0x03, 0xFC, 0x70, 0xE6, 0x06, 0xC0, 0x3C, 0xF3, 0xCF, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x70, 0xE3, 0xFC, 0x1F, 0x80, +/* 0xC9 */ 0xFF, 0xFF, 0xFF, 0xC0, +/* 0xCA */ 0xC1, 0xD8, 0x73, 0x1C, 0x67, 0x0D, 0xC1, 0xF0, 0x3F, 0x07, 0x70, 0xC7, 0x18, 0x63, 0x0E, 0x60, 0xEC, 0x0E, +/* 0xCB */ 0x07, 0x00, 0x38, 0x01, 0xC0, 0x1B, 0x00, 0xD8, 0x0C, 0x60, 0x63, 0x03, 0x18, 0x30, 0x61, 0x83, 0x1C, 0x1C, 0xC0, 0x66, 0x03, 0x00, +/* 0xCC */ 0xE0, 0x3F, 0x83, 0xFC, 0x1F, 0xE0, 0xFD, 0x8D, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF3, 0x67, 0x8E, 0x3C, 0x71, 0x80, +/* 0xCD */ 0xC0, 0x7C, 0x0F, 0xC1, 0xF8, 0x3D, 0x87, 0x98, 0xF3, 0x9E, 0x33, 0xC3, 0x78, 0x3F, 0x07, 0xE0, 0x7C, 0x06, +/* 0xCE */ 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x1F, 0xE7, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xC0, +/* 0xCF */ 0x0F, 0x83, 0xFC, 0x70, 0xE6, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x70, 0xE3, 0xFC, 0x0F, 0x00, +/* 0xD0 */ 0xFF, 0xFF, 0xFF, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, +/* 0xD1 */ 0xFF, 0x3F, 0xEC, 0x1F, 0x03, 0xC0, 0xF0, 0x7F, 0xFB, 0xFC, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, +/* 0xD2 */ +/* 0xD3 */ 0xFF, 0xFF, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0xFF, 0xFF, +/* 0xD4 */ 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, +/* 0xD5 */ 0xE0, 0x76, 0x06, 0x30, 0xC3, 0x9C, 0x19, 0x80, 0xF0, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, +/* 0xD6 */ 0x06, 0x00, 0x60, 0x1F, 0x87, 0xFE, 0xE6, 0x7C, 0x63, 0xC6, 0x3C, 0x63, 0xE6, 0x77, 0xFE, 0x1F, 0x80, 0x60, 0x06, 0x00, +/* 0xD7 */ 0x71, 0xC6, 0x30, 0x6C, 0x0D, 0x80, 0xE0, 0x1C, 0x03, 0x80, 0xD8, 0x1B, 0x07, 0x70, 0xC6, 0x30, 0x6E, 0x0E, +/* 0xD8 */ 0xC6, 0x3C, 0x63, 0xC6, 0x3C, 0x63, 0xC6, 0x3C, 0x63, 0x46, 0x66, 0x66, 0x3F, 0xC0, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, +/* 0xD9 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0x40, 0x4C, 0x18, 0xEE, 0x7D, 0xFF, 0xBE, +/* 0xDA */ 0xCF, 0x30, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, +/* 0xDB */ 0x19, 0x81, 0x98, 0x00, 0x0E, 0x07, 0x60, 0x63, 0x0C, 0x39, 0xC1, 0x98, 0x0F, 0x00, 0xF0, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, +/* 0xDC */ 0x06, 0x0C, 0x00, 0x3B, 0x7B, 0xEE, 0xC6, 0xC6, 0xC6, 0xC6, 0xEE, 0x7B, 0x3B, +/* 0xDD */ 0x18, 0x20, 0x03, 0xCF, 0xF8, 0xB0, 0x38, 0x71, 0x83, 0x17, 0xF7, 0x80, +/* 0xDE */ 0x0C, 0x18, 0x00, 0xDE, 0xFF, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x03, 0x03, 0x03, 0x03, +/* 0xDF */ 0x78, 0x6D, 0xB6, 0xDB, 0x6C, +/* 0xE0 */ 0x0C, 0xDB, 0xD3, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, +/* 0xE1 */ 0x3B, 0x7B, 0xEE, 0xC6, 0xC6, 0xC6, 0xC6, 0xEE, 0x7B, 0x3B, +/* 0xE2 */ 0x3C, 0x7E, 0xC6, 0xC6, 0xC4, 0xD8, 0xDE, 0xC7, 0xC3, 0xC3, 0xE7, 0xFE, 0xDC, 0xC0, 0xC0, 0xC0, 0xC0, +/* 0xE3 */ 0x61, 0x98, 0x66, 0x18, 0xCC, 0x33, 0x0C, 0xC1, 0xE0, 0x78, 0x1E, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, +/* 0xE4 */ 0x7E, 0x7E, 0x30, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, +/* 0xE5 */ 0x79, 0xFF, 0x16, 0x07, 0x0E, 0x30, 0x62, 0xFE, 0xF0, +/* 0xE6 */ 0x7E, 0xFC, 0x30, 0xC3, 0x0C, 0x18, 0x60, 0xC1, 0x83, 0x07, 0xE7, 0xE0, 0xC1, 0x83, 0x0C, +/* 0xE7 */ 0xDE, 0xFF, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x03, 0x03, 0x03, 0x03, +/* 0xE8 */ 0x3C, 0x7E, 0x66, 0xC3, 0xC3, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0x66, 0x7E, 0x3C, +/* 0xE9 */ 0xFF, 0xFF, 0xF0, +/* 0xEA */ 0xC3, 0x63, 0x33, 0x1B, 0x0F, 0x06, 0xC3, 0x31, 0x8C, 0xC6, 0x61, 0x80, +/* 0xEB */ 0x30, 0x0C, 0x06, 0x03, 0x01, 0xC1, 0xE0, 0xD0, 0x6C, 0x36, 0x33, 0x18, 0xCC, 0x66, 0x30, +/* 0xEC */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0xFF, 0xDB, 0xC0, 0xC0, 0xC0, 0xC0, +/* 0xED */ 0xC1, 0xE0, 0xD8, 0xCC, 0x66, 0x31, 0xB0, 0xD8, 0x38, 0x1C, 0x04, 0x00, +/* 0xEE */ 0x7D, 0xFB, 0x06, 0x07, 0xC7, 0x9C, 0x70, 0xC1, 0x83, 0x83, 0xE3, 0xE0, 0xC1, 0x8E, 0x18, +/* 0xEF */ 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, +/* 0xF0 */ 0xFF, 0xFF, 0xFF, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, +/* 0xF1 */ 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0xFE, 0xDC, 0xC0, 0xC0, 0xC0, 0xC0, +/* 0xF2 */ 0x1E, 0xFD, 0x86, 0x0C, 0x18, 0x30, 0x70, 0x7C, 0x7C, 0x18, 0x33, 0xE7, 0x00, +/* 0xF3 */ 0x3F, 0xDF, 0xFE, 0x63, 0x0C, 0xC3, 0x30, 0xCC, 0x33, 0x9C, 0x7E, 0x0F, 0x00, +/* 0xF4 */ 0xFF, 0xF3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, +/* 0xF5 */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, +/* 0xF6 */ 0x2F, 0x1B, 0xEC, 0xDF, 0x33, 0xCC, 0xF3, 0x3C, 0xCD, 0xB6, 0x7F, 0x8F, 0x80, 0xC0, 0x30, 0x0C, 0x03, 0x00, +/* 0xF7 */ 0x63, 0x31, 0x8D, 0x86, 0xC3, 0x60, 0xE0, 0x70, 0x38, 0x1C, 0x1B, 0x0D, 0x86, 0xC6, 0x33, 0x18, +/* 0xF8 */ 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0x6D, 0x8F, 0xC0, 0xC0, 0x30, 0x0C, 0x03, 0x00, +/* 0xF9 */ 0x30, 0xC6, 0x06, 0x66, 0x6C, 0x63, 0xC6, 0x3C, 0x63, 0xC6, 0x3E, 0xF7, 0x79, 0xE3, 0x9C, +/* 0xFA */ 0xCF, 0x30, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, +/* 0xFB */ 0x66, 0x66, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, +/* 0xFC */ 0x0C, 0x18, 0x00, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, +/* 0xFD */ 0x08, 0x10, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, +/* 0xFE */ 0x03, 0x00, 0x60, 0x00, 0x03, 0x0C, 0x60, 0x66, 0x66, 0xC6, 0x3C, 0x63, 0xC6, 0x3C, 0x63, 0xEF, 0x77, 0x9E, 0x39, 0xC0, +/* 0xFF */ +}; + +const GFXglyph FreeSans9pt_Win1253Glyphs[] PROGMEM = { +/* 0x01 */ { 0, 15, 15, 17, 1, -13 }, +/* 0x02 */ { 29, 15, 15, 17, 1, -13 }, +/* 0x03 */ { 58, 15, 16, 17, 1, -14 }, +/* 0x04 */ { 88, 15, 16, 17, 1, -14 }, +/* 0x05 */ { 118, 16, 15, 18, 1, -13 }, +/* 0x06 */ { 148, 15, 15, 17, 1, -13 }, +/* 0x07 */ { 177, 0, 0, 8, 0, 0 }, +/* 0x08 */ { 177, 17, 16, 19, 1, -14 }, +/* 0x09 */ { 211, 17, 12, 19, 1, -12 }, +/* 0x0A */ { 237, 0, 0, 8, 0, 0 }, +/* 0x0B */ { 237, 17, 16, 19, 1, -14 }, +/* 0x0C */ { 271, 15, 14, 17, 1, -12 }, +/* 0x0D */ { 298, 0, 0, 8, 0, 0 }, +/* 0x0E */ { 298, 15, 16, 17, 1, -14 }, +/* 0x0F */ { 328, 15, 15, 17, 1, -13 }, +/* 0x10 */ { 357, 15, 15, 17, 1, -13 }, +/* 0x11 */ { 386, 15, 16, 17, 1, -14 }, +/* 0x12 */ { 416, 17, 17, 19, 1, -15 }, +/* 0x13 */ { 453, 15, 16, 17, 1, -14 }, +/* 0x14 */ { 483, 15, 16, 17, 1, -14 }, +/* 0x15 */ { 513, 15, 16, 17, 1, -14 }, +/* 0x16 */ { 543, 11, 16, 13, 1, -14 }, +/* 0x17 */ { 565, 15, 16, 17, 1, -14 }, +/* 0x18 */ { 595, 18, 15, 20, 1, -13 }, +/* 0x19 */ { 629, 15, 16, 17, 1, -14 }, +/* 0x1A */ { 659, 13, 14, 15, 1, -12 }, +/* 0x1B */ { 682, 17, 16, 19, 1, -14 }, +/* 0x1C */ { 716, 15, 16, 17, 1, -14 }, +/* 0x1D */ { 746, 15, 15, 17, 1, -13 }, +/* 0x1E */ { 775, 17, 16, 19, 1, -14 }, +/* 0x1F */ { 809, 11, 16, 13, 1, -14 }, +/* ' ' 0x20 */ { 831, 0, 0, 5, 0, 0 }, +/* '!' 0x21 */ { 831, 2, 13, 6, 2, -12 }, +/* '"' 0x22 */ { 835, 5, 4, 6, 1, -12 }, +/* '#' 0x23 */ { 838, 10, 12, 10, 0, -11 }, +/* '$' 0x24 */ { 853, 9, 16, 10, 1, -13 }, +/* '%' 0x25 */ { 871, 16, 13, 16, 1, -12 }, +/* '&' 0x26 */ { 897, 10, 13, 12, 1, -12 }, +/* ''' 0x27 */ { 914, 2, 4, 4, 1, -12 }, +/* '(' 0x28 */ { 915, 4, 17, 6, 1, -12 }, +/* ')' 0x29 */ { 924, 4, 17, 6, 1, -12 }, +/* '*' 0x2A */ { 933, 5, 5, 7, 1, -12 }, +/* '+' 0x2B */ { 937, 6, 8, 11, 3, -7 }, +/* ',' 0x2C */ { 943, 2, 4, 5, 2, 0 }, +/* '-' 0x2D */ { 944, 4, 1, 6, 1, -4 }, +/* '.' 0x2E */ { 945, 2, 1, 5, 1, 0 }, +/* '/' 0x2F */ { 946, 5, 13, 5, 0, -12 }, +/* '0' 0x30 */ { 955, 8, 13, 10, 1, -12 }, +/* '1' 0x31 */ { 968, 4, 13, 10, 3, -12 }, +/* '2' 0x32 */ { 975, 9, 13, 10, 1, -12 }, +/* '3' 0x33 */ { 990, 8, 13, 10, 1, -12 }, +/* '4' 0x34 */ { 1003, 7, 13, 10, 2, -12 }, +/* '5' 0x35 */ { 1015, 9, 13, 10, 1, -12 }, +/* '6' 0x36 */ { 1030, 9, 13, 10, 1, -12 }, +/* '7' 0x37 */ { 1045, 8, 13, 10, 0, -12 }, +/* '8' 0x38 */ { 1058, 9, 13, 10, 1, -12 }, +/* '9' 0x39 */ { 1073, 8, 13, 10, 1, -12 }, +/* ':' 0x3A */ { 1086, 2, 10, 5, 1, -9 }, +/* ';' 0x3B */ { 1089, 3, 12, 5, 1, -8 }, +/* '<' 0x3C */ { 1094, 9, 9, 11, 1, -8 }, +/* '=' 0x3D */ { 1105, 9, 4, 11, 1, -5 }, +/* '>' 0x3E */ { 1110, 9, 8, 11, 1, -7 }, +/* '?' 0x3F */ { 1119, 9, 13, 10, 1, -12 }, +/* '@' 0x40 */ { 1134, 17, 16, 18, 1, -12 }, +/* 'A' 0x41 */ { 1168, 12, 13, 12, 0, -12 }, +/* 'B' 0x42 */ { 1188, 11, 13, 12, 1, -12 }, +/* 'C' 0x43 */ { 1206, 11, 13, 13, 1, -12 }, +/* 'D' 0x44 */ { 1224, 11, 13, 13, 1, -12 }, +/* 'E' 0x45 */ { 1242, 9, 13, 11, 1, -12 }, +/* 'F' 0x46 */ { 1257, 8, 13, 11, 1, -12 }, +/* 'G' 0x47 */ { 1270, 12, 13, 14, 1, -12 }, +/* 'H' 0x48 */ { 1290, 11, 13, 13, 1, -12 }, +/* 'I' 0x49 */ { 1308, 2, 13, 5, 2, -12 }, +/* 'J' 0x4A */ { 1312, 7, 13, 10, 1, -12 }, +/* 'K' 0x4B */ { 1324, 10, 13, 12, 1, -12 }, +/* 'L' 0x4C */ { 1341, 8, 13, 10, 1, -12 }, +/* 'M' 0x4D */ { 1354, 13, 13, 15, 1, -12 }, +/* 'N' 0x4E */ { 1376, 11, 13, 13, 1, -12 }, +/* 'O' 0x4F */ { 1394, 13, 13, 14, 1, -12 }, +/* 'P' 0x50 */ { 1416, 10, 13, 12, 1, -12 }, +/* 'Q' 0x51 */ { 1433, 13, 14, 14, 1, -12 }, +/* 'R' 0x52 */ { 1456, 12, 13, 13, 1, -12 }, +/* 'S' 0x53 */ { 1476, 10, 13, 12, 1, -12 }, +/* 'T' 0x54 */ { 1493, 9, 13, 11, 1, -12 }, +/* 'U' 0x55 */ { 1508, 11, 13, 13, 1, -12 }, +/* 'V' 0x56 */ { 1526, 11, 13, 11, 0, -12 }, +/* 'W' 0x57 */ { 1544, 16, 13, 17, 0, -12 }, +/* 'X' 0x58 */ { 1570, 10, 13, 12, 1, -12 }, +/* 'Y' 0x59 */ { 1587, 12, 13, 12, 0, -12 }, +/* 'Z' 0x5A */ { 1607, 10, 13, 11, 1, -12 }, +/* '[' 0x5B */ { 1624, 3, 17, 5, 1, -12 }, +/* '\' 0x5C */ { 1631, 5, 13, 5, 0, -12 }, +/* ']' 0x5D */ { 1640, 3, 17, 5, 0, -12 }, +/* '^' 0x5E */ { 1647, 7, 7, 8, 1, -12 }, +/* '_' 0x5F */ { 1654, 10, 1, 10, 0, 3 }, +/* '`' 0x60 */ { 1656, 4, 3, 5, 0, -12 }, +/* 'a' 0x61 */ { 1658, 9, 10, 10, 1, -9 }, +/* 'b' 0x62 */ { 1670, 9, 13, 10, 1, -12 }, +/* 'c' 0x63 */ { 1685, 8, 10, 9, 1, -9 }, +/* 'd' 0x64 */ { 1695, 8, 13, 10, 1, -12 }, +/* 'e' 0x65 */ { 1708, 8, 10, 10, 1, -9 }, +/* 'f' 0x66 */ { 1718, 4, 13, 5, 1, -12 }, +/* 'g' 0x67 */ { 1725, 8, 14, 10, 1, -9 }, +/* 'h' 0x68 */ { 1739, 8, 13, 10, 1, -12 }, +/* 'i' 0x69 */ { 1752, 2, 13, 4, 1, -12 }, +/* 'j' 0x6A */ { 1756, 4, 17, 4, 0, -12 }, +/* 'k' 0x6B */ { 1765, 8, 13, 9, 1, -12 }, +/* 'l' 0x6C */ { 1778, 2, 13, 4, 1, -12 }, +/* 'm' 0x6D */ { 1782, 13, 10, 15, 1, -9 }, +/* 'n' 0x6E */ { 1799, 8, 10, 10, 1, -9 }, +/* 'o' 0x6F */ { 1809, 8, 10, 10, 1, -9 }, +/* 'p' 0x70 */ { 1819, 9, 13, 10, 1, -9 }, +/* 'q' 0x71 */ { 1834, 8, 13, 10, 1, -9 }, +/* 'r' 0x72 */ { 1847, 5, 10, 6, 1, -9 }, +/* 's' 0x73 */ { 1854, 8, 10, 9, 1, -9 }, +/* 't' 0x74 */ { 1864, 4, 12, 5, 1, -11 }, +/* 'u' 0x75 */ { 1870, 8, 10, 10, 1, -9 }, +/* 'v' 0x76 */ { 1880, 9, 10, 9, 0, -9 }, +/* 'w' 0x77 */ { 1892, 13, 10, 13, 0, -9 }, +/* 'x' 0x78 */ { 1909, 7, 10, 9, 1, -9 }, +/* 'y' 0x79 */ { 1918, 8, 14, 9, 0, -9 }, +/* 'z' 0x7A */ { 1932, 7, 10, 9, 1, -9 }, +/* '{' 0x7B */ { 1941, 4, 17, 6, 1, -12 }, +/* '|' 0x7C */ { 1950, 2, 17, 4, 2, -12 }, +/* '}' 0x7D */ { 1955, 4, 17, 6, 1, -12 }, +/* '~' 0x7E */ { 1964, 7, 3, 9, 1, -7 }, +/* 0x7F */ { 1967, 0, 0, 0, 0, 0 }, +/* 0x80 */ { 1967, 10, 13, 12, 1, -12 }, +/* 0x81 */ { 1984, 0, 0, 8, 0, 0 }, +/* 0x82 */ { 1984, 2, 3, 5, 1, 0 }, +/* 0x83 */ { 1985, 5, 17, 5, 0, -12 }, +/* 0x84 */ { 1996, 5, 3, 7, 1, 0 }, +/* 0x85 */ { 1998, 10, 1, 12, 1, 0 }, +/* 0x86 */ { 2000, 8, 16, 10, 1, -12 }, +/* 0x87 */ { 2016, 8, 16, 10, 1, -12 }, +/* 0x88 */ { 2032, 5, 3, 6, 0, -12 }, +/* 0x89 */ { 2034, 18, 13, 18, 0, -12 }, +/* 0x8A */ { 2064, 10, 16, 12, 1, -15 }, +/* 0x8B */ { 2084, 2, 4, 4, 1, -6 }, +/* 0x8C */ { 2085, 15, 13, 18, 1, -12 }, +/* 0x8D */ { 2110, 0, 0, 8, 0, 0 }, +/* 0x8E */ { 2110, 10, 16, 11, 1, -15 }, +/* 0x8F */ { 2130, 0, 0, 8, 0, 0 }, +/* 0x90 */ { 2130, 0, 0, 8, 0, 0 }, +/* 0x91 */ { 2130, 2, 4, 4, 2, -12 }, +/* 0x92 */ { 2131, 2, 4, 4, 1, -12 }, +/* 0x93 */ { 2132, 5, 4, 7, 2, -12 }, +/* 0x94 */ { 2135, 5, 4, 7, 1, -12 }, +/* 0x95 */ { 2138, 4, 5, 7, 1, -8 }, +/* 0x96 */ { 2141, 7, 1, 9, 1, -4 }, +/* 0x97 */ { 2142, 16, 1, 18, 1, -4 }, +/* 0x98 */ { 2144, 5, 2, 6, 0, -12 }, +/* 0x99 */ { 2146, 18, 10, 18, 1, -13 }, +/* 0x9A */ { 2169, 8, 13, 9, 1, -12 }, +/* 0x9B */ { 2182, 2, 4, 5, 2, -6 }, +/* 0x9C */ { 2183, 15, 10, 17, 1, -9 }, +/* 0x9D */ { 2202, 0, 0, 8, 0, 0 }, +/* 0x9E */ { 2202, 7, 13, 9, 1, -12 }, +/* 0x9F */ { 2214, 12, 14, 12, 0, -13 }, +/* 0xA0 */ { 2235, 0, 0, 5, 0, 0 }, +/* 0xA1 */ { 2235, 2, 13, 6, 2, -8 }, +/* 0xA2 */ { 2239, 9, 14, 10, 1, -11 }, +/* 0xA3 */ { 2255, 10, 13, 10, 0, -12 }, +/* 0xA4 */ { 2272, 7, 6, 10, 2, -8 }, +/* 0xA5 */ { 2278, 8, 13, 10, 1, -12 }, +/* 0xA6 */ { 2291, 2, 17, 5, 2, -12 }, +/* 0xA7 */ { 2296, 9, 17, 10, 1, -12 }, +/* 0xA8 */ { 2316, 6, 1, 6, 0, -11 }, +/* 0xA9 */ { 2317, 14, 13, 14, 1, -12 }, +/* 0xAA */ { 2340, 5, 8, 7, 1, -12 }, +/* 0xAB */ { 2345, 7, 6, 9, 1, -7 }, +/* 0xAC */ { 2351, 9, 5, 11, 2, -5 }, +/* 0xAD */ { 2357, 0, 0, 0, 0, 0 }, +/* 0xAE */ { 2357, 14, 13, 14, 1, -12 }, +/* 0xAF */ { 2380, 5, 1, 6, 0, -12 }, +/* 0xB0 */ { 2381, 5, 5, 11, 3, -11 }, +/* 0xB1 */ { 2385, 9, 11, 11, 1, -10 }, +/* 0xB2 */ { 2398, 6, 8, 6, 1, -13 }, +/* 0xB3 */ { 2404, 7, 8, 6, 0, -13 }, +/* 0xB4 */ { 2411, 4, 3, 6, 2, -12 }, +/* 0xB5 */ { 2413, 9, 13, 10, 1, -9 }, +/* 0xB6 */ { 2428, 8, 16, 10, 2, -12 }, +/* 0xB7 */ { 2444, 3, 1, 5, 1, -4 }, +/* 0xB8 */ { 2445, 5, 4, 6, 1, 1 }, +/* 0xB9 */ { 2448, 3, 7, 6, 2, -13 }, +/* 0xBA */ { 2451, 5, 8, 7, 1, -12 }, +/* 0xBB */ { 2456, 7, 6, 9, 1, -7 }, +/* 0xBC */ { 2462, 14, 13, 16, 2, -12 }, +/* 0xBD */ { 2485, 14, 13, 16, 2, -12 }, +/* 0xBE */ { 2508, 15, 13, 16, 1, -12 }, +/* 0xBF */ { 2533, 9, 13, 10, 1, -8 }, +/* 0xC0 */ { 2548, 8, 15, 4, -2, -15 }, +/* 0xC1 */ { 2563, 11, 13, 11, 0, -13 }, +/* 0xC2 */ { 2581, 10, 13, 12, 1, -13 }, +/* 0xC3 */ { 2598, 8, 13, 10, 2, -13 }, +/* 0xC4 */ { 2611, 13, 13, 12, -1, -13 }, +/* 0xC5 */ { 2633, 10, 13, 12, 1, -13 }, +/* 0xC6 */ { 2650, 10, 13, 11, 0, -13 }, +/* 0xC7 */ { 2667, 11, 13, 13, 1, -13 }, +/* 0xC8 */ { 2685, 12, 13, 14, 1, -13 }, +/* 0xC9 */ { 2705, 2, 13, 4, 1, -13 }, +/* 0xCA */ { 2709, 11, 13, 12, 1, -13 }, +/* 0xCB */ { 2727, 13, 13, 12, -1, -13 }, +/* 0xCC */ { 2749, 13, 13, 15, 1, -13 }, +/* 0xCD */ { 2771, 11, 13, 13, 1, -13 }, +/* 0xCE */ { 2789, 10, 13, 12, 1, -13 }, +/* 0xCF */ { 2806, 12, 13, 14, 1, -13 }, +/* 0xD0 */ { 2826, 11, 13, 13, 1, -13 }, +/* 0xD1 */ { 2844, 10, 13, 12, 1, -13 }, +/* 0xD2 */ { 2861, 0, 0, 5, 0, 0 }, +/* 0xD3 */ { 2861, 8, 13, 11, 2, -13 }, +/* 0xD4 */ { 2874, 10, 13, 12, 1, -13 }, +/* 0xD5 */ { 2891, 12, 13, 12, 0, -13 }, +/* 0xD6 */ { 2911, 12, 13, 14, 1, -13 }, +/* 0xD7 */ { 2931, 11, 13, 11, 0, -13 }, +/* 0xD8 */ { 2949, 12, 13, 14, 1, -13 }, +/* 0xD9 */ { 2969, 11, 13, 13, 1, -13 }, +/* 0xDA */ { 2987, 6, 16, 4, -1, -16 }, +/* 0xDB */ { 2999, 12, 16, 12, 0, -16 }, +/* 0xDC */ { 3023, 8, 13, 10, 1, -13 }, +/* 0xDD */ { 3036, 7, 13, 8, 1, -13 }, +/* 0xDE */ { 3048, 8, 17, 10, 1, -13 }, +/* 0xDF */ { 3065, 3, 13, 4, 1, -13 }, +/* 0xE0 */ { 3070, 8, 14, 10, 1, -14 }, +/* 0xE1 */ { 3084, 8, 10, 10, 1, -10 }, +/* 0xE2 */ { 3094, 8, 17, 10, 1, -13 }, +/* 0xE3 */ { 3111, 10, 14, 8, -1, -10 }, +/* 0xE4 */ { 3129, 8, 13, 10, 1, -13 }, +/* 0xE5 */ { 3142, 7, 10, 8, 1, -10 }, +/* 0xE6 */ { 3151, 7, 17, 8, 1, -13 }, +/* 0xE7 */ { 3166, 8, 14, 10, 1, -10 }, +/* 0xE8 */ { 3180, 8, 13, 10, 1, -13 }, +/* 0xE9 */ { 3193, 2, 10, 4, 1, -10 }, +/* 0xEA */ { 3196, 9, 10, 9, 1, -10 }, +/* 0xEB */ { 3208, 9, 13, 9, 0, -13 }, +/* 0xEC */ { 3223, 8, 14, 10, 1, -10 }, +/* 0xED */ { 3237, 9, 10, 9, 0, -10 }, +/* 0xEE */ { 3249, 7, 17, 8, 1, -13 }, +/* 0xEF */ { 3264, 8, 10, 10, 1, -10 }, +/* 0xF0 */ { 3274, 12, 10, 12, 0, -10 }, +/* 0xF1 */ { 3289, 8, 14, 10, 1, -10 }, +/* 0xF2 */ { 3303, 7, 14, 9, 1, -10 }, +/* 0xF3 */ { 3316, 10, 10, 11, 1, -10 }, +/* 0xF4 */ { 3329, 6, 10, 8, 1, -10 }, +/* 0xF5 */ { 3337, 8, 10, 10, 1, -10 }, +/* 0xF6 */ { 3347, 10, 14, 12, 1, -10 }, +/* 0xF7 */ { 3365, 9, 14, 9, 0, -10 }, +/* 0xF8 */ { 3381, 10, 14, 12, 1, -10 }, +/* 0xF9 */ { 3399, 12, 10, 14, 1, -10 }, +/* 0xFA */ { 3414, 6, 13, 4, -1, -13 }, +/* 0xFB */ { 3424, 8, 13, 10, 1, -13 }, +/* 0xFC */ { 3437, 8, 13, 10, 1, -13 }, +/* 0xFD */ { 3450, 8, 13, 10, 1, -13 }, +/* 0xFE */ { 3463, 12, 13, 14, 1, -13 }, +/* 0xFF */ { 3483, 0, 0, 5, 0, 0 }, +}; + +const GFXfont FreeSans9pt_Win1253 PROGMEM = { +(uint8_t*)FreeSans9pt_Win1253Bitmaps, +(GFXglyph*)FreeSans9pt_Win1253Glyphs, +0x01, 0xFF, 16 +}; diff --git a/src/graphics/niche/InkHUD/Applet.cpp b/src/graphics/niche/InkHUD/Applet.cpp index 1e89ebe1b..ccdd76f97 100644 --- a/src/graphics/niche/InkHUD/Applet.cpp +++ b/src/graphics/niche/InkHUD/Applet.cpp @@ -55,7 +55,7 @@ InkHUD::Tile *InkHUD::Applet::getTile() } // Draw the applet -void InkHUD::Applet::render() +void InkHUD::Applet::render(bool full) { assert(assignedTile); // Ensure that we have a tile assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile @@ -65,10 +65,11 @@ void InkHUD::Applet::render() wantRender = false; // Flag set by requestUpdate wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored. wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted. + wantFullRender = true; // Default to a full render updateDimensions(); resetDrawingSpace(); - onRender(); // Derived applet's drawing takes place here + onRender(full); // Draw the applet // Handle "Tile Highlighting" // Some devices may use an auxiliary button to switch between tiles @@ -115,6 +116,11 @@ Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType() return wantUpdateType; } +bool InkHUD::Applet::wantsFullRender() +{ + return wantFullRender; +} + // Get size of the applet's drawing space from its tile // Performed immediately before derived applet's drawing code runs void InkHUD::Applet::updateDimensions() @@ -142,10 +148,11 @@ void InkHUD::Applet::resetDrawingSpace() // Once the renderer has given other applets a chance to process whatever event we just detected, // it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground) // We should requestUpdate even if our applet is currently background, because this might be changed by autoshow -void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type) +void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type, bool full) { wantRender = true; wantUpdateType = type; + wantFullRender = full; inkhud->requestUpdate(); } diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h index 802186e6e..69d35a234 100644 --- a/src/graphics/niche/InkHUD/Applet.h +++ b/src/graphics/niche/InkHUD/Applet.h @@ -64,10 +64,11 @@ class Applet : public GFX // Rendering - void render(); // Draw the applet + void render(bool full); // Draw the applet bool wantsToRender(); // Check whether applet wants to render bool wantsToAutoshow(); // Check whether applet wants to become foreground Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer + bool wantsFullRender(); // Check whether applet wants to render over its previous render void updateDimensions(); // Get current size from tile void resetDrawingSpace(); // Makes sure every render starts with same parameters @@ -82,14 +83,23 @@ class Applet : public GFX // Event handlers - virtual void onRender() = 0; // All drawing happens here + virtual void onRender(bool full) = 0; // For drawing the applet virtual void onActivate() {} virtual void onDeactivate() {} virtual void onForeground() {} virtual void onBackground() {} virtual void onShutdown() {} - virtual void onButtonShortPress() {} // (System Applets only) - virtual void onButtonLongPress() {} // (System Applets only) + virtual void onButtonShortPress() {} + virtual void onButtonLongPress() {} + virtual void onExitShort() {} + virtual void onExitLong() {} + virtual void onNavUp() {} + virtual void onNavDown() {} + virtual void onNavLeft() {} + virtual void onNavRight() {} + virtual void onFreeText(char c) {} + virtual void onFreeTextDone() {} + virtual void onFreeTextCancel() {} virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification @@ -102,8 +112,9 @@ class Applet : public GFX protected: void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here - void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update - void requestAutoshow(); // Ask for applet to be moved to foreground + void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED, + bool full = true); // Ask WindowManager to schedule a display update + void requestAutoshow(); // Ask for applet to be moved to foreground uint16_t X(float f); // Map applet width, mapped from 0 to 1.0 uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0 @@ -158,6 +169,7 @@ class Applet : public GFX bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground? NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType = NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display + bool wantFullRender = true; // Render with a fresh canvas using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager. diff --git a/src/graphics/niche/InkHUD/AppletFont.cpp b/src/graphics/niche/InkHUD/AppletFont.cpp index 6c7a7b491..93a621ee8 100644 --- a/src/graphics/niche/InkHUD/AppletFont.cpp +++ b/src/graphics/niche/InkHUD/AppletFont.cpp @@ -616,6 +616,101 @@ char InkHUD::AppletFont::applyEncoding(std::string utf8) } } + else if (encoding == WINDOWS_1253) { + // Greek + // 1-Byte chars: no remapping + if (utf8.length() == 1) + return utf8.at(0); + + // Multi-byte chars: + switch (toUtf32(utf8)) { + // Windows-1253 special characters (0x80-0xBF range) + REMAP(0x20AC, 0x80) // EURO SIGN + REMAP(0x2018, 0x91) // LEFT SINGLE QUOTATION MARK + REMAP(0x2019, 0x92) // RIGHT SINGLE QUOTATION MARK + REMAP(0x201C, 0x93) // LEFT DOUBLE QUOTATION MARK + REMAP(0x201D, 0x94) // RIGHT DOUBLE QUOTATION MARK + REMAP(0x2022, 0x95) // BULLET + REMAP(0x2013, 0x96) // EN DASH + REMAP(0x2014, 0x97) // EM DASH + + // Greek accented capitals + REMAP(0x0386, 0xA2) // GREEK CAPITAL LETTER ALPHA WITH TONOS + REMAP(0x0388, 0xB8) // GREEK CAPITAL LETTER EPSILON WITH TONOS + REMAP(0x0389, 0xB9) // GREEK CAPITAL LETTER ETA WITH TONOS + REMAP(0x038A, 0xBA) // GREEK CAPITAL LETTER IOTA WITH TONOS + REMAP(0x038C, 0xBC) // GREEK CAPITAL LETTER OMICRON WITH TONOS + REMAP(0x038E, 0xBE) // GREEK CAPITAL LETTER UPSILON WITH TONOS + REMAP(0x038F, 0xBF) // GREEK CAPITAL LETTER OMEGA WITH TONOS + + // Greek capital letters (U+0391-U+03A9 -> 0xC1-0xD1, with gaps) + REMAP(0x0391, 0xC1) // GREEK CAPITAL LETTER ALPHA + REMAP(0x0392, 0xC2) // GREEK CAPITAL LETTER BETA + REMAP(0x0393, 0xC3) // GREEK CAPITAL LETTER GAMMA + REMAP(0x0394, 0xC4) // GREEK CAPITAL LETTER DELTA + REMAP(0x0395, 0xC5) // GREEK CAPITAL LETTER EPSILON + REMAP(0x0396, 0xC6) // GREEK CAPITAL LETTER ZETA + REMAP(0x0397, 0xC7) // GREEK CAPITAL LETTER ETA + REMAP(0x0398, 0xC8) // GREEK CAPITAL LETTER THETA + REMAP(0x0399, 0xC9) // GREEK CAPITAL LETTER IOTA + REMAP(0x039A, 0xCA) // GREEK CAPITAL LETTER KAPPA + REMAP(0x039B, 0xCB) // GREEK CAPITAL LETTER LAMDA + REMAP(0x039C, 0xCC) // GREEK CAPITAL LETTER MU + REMAP(0x039D, 0xCD) // GREEK CAPITAL LETTER NU + REMAP(0x039E, 0xCE) // GREEK CAPITAL LETTER XI + REMAP(0x039F, 0xCF) // GREEK CAPITAL LETTER OMICRON + REMAP(0x03A0, 0xD0) // GREEK CAPITAL LETTER PI + REMAP(0x03A1, 0xD1) // GREEK CAPITAL LETTER RHO + REMAP(0x03A3, 0xD3) // GREEK CAPITAL LETTER SIGMA + REMAP(0x03A4, 0xD4) // GREEK CAPITAL LETTER TAU + REMAP(0x03A5, 0xD5) // GREEK CAPITAL LETTER UPSILON + REMAP(0x03A6, 0xD6) // GREEK CAPITAL LETTER PHI + REMAP(0x03A7, 0xD7) // GREEK CAPITAL LETTER CHI + REMAP(0x03A8, 0xD8) // GREEK CAPITAL LETTER PSI + REMAP(0x03A9, 0xD9) // GREEK CAPITAL LETTER OMEGA + + // Greek small letters with tonos (accented) + REMAP(0x03AC, 0xDC) // GREEK SMALL LETTER ALPHA WITH TONOS + REMAP(0x03AD, 0xDD) // GREEK SMALL LETTER EPSILON WITH TONOS + REMAP(0x03AE, 0xDE) // GREEK SMALL LETTER ETA WITH TONOS + REMAP(0x03AF, 0xDF) // GREEK SMALL LETTER IOTA WITH TONOS + + // Greek small letters (U+03B1-U+03C9 -> 0xE1-0xF9) + REMAP(0x03B1, 0xE1) // GREEK SMALL LETTER ALPHA + REMAP(0x03B2, 0xE2) // GREEK SMALL LETTER BETA + REMAP(0x03B3, 0xE3) // GREEK SMALL LETTER GAMMA + REMAP(0x03B4, 0xE4) // GREEK SMALL LETTER DELTA + REMAP(0x03B5, 0xE5) // GREEK SMALL LETTER EPSILON + REMAP(0x03B6, 0xE6) // GREEK SMALL LETTER ZETA + REMAP(0x03B7, 0xE7) // GREEK SMALL LETTER ETA + REMAP(0x03B8, 0xE8) // GREEK SMALL LETTER THETA + REMAP(0x03B9, 0xE9) // GREEK SMALL LETTER IOTA + REMAP(0x03BA, 0xEA) // GREEK SMALL LETTER KAPPA + REMAP(0x03BB, 0xEB) // GREEK SMALL LETTER LAMDA + REMAP(0x03BC, 0xEC) // GREEK SMALL LETTER MU + REMAP(0x03BD, 0xED) // GREEK SMALL LETTER NU + REMAP(0x03BE, 0xEE) // GREEK SMALL LETTER XI + REMAP(0x03BF, 0xEF) // GREEK SMALL LETTER OMICRON + REMAP(0x03C0, 0xF0) // GREEK SMALL LETTER PI + REMAP(0x03C1, 0xF1) // GREEK SMALL LETTER RHO + REMAP(0x03C2, 0xF2) // GREEK SMALL LETTER FINAL SIGMA + REMAP(0x03C3, 0xF3) // GREEK SMALL LETTER SIGMA + REMAP(0x03C4, 0xF4) // GREEK SMALL LETTER TAU + REMAP(0x03C5, 0xF5) // GREEK SMALL LETTER UPSILON + REMAP(0x03C6, 0xF6) // GREEK SMALL LETTER PHI + REMAP(0x03C7, 0xF7) // GREEK SMALL LETTER CHI + REMAP(0x03C8, 0xF8) // GREEK SMALL LETTER PSI + REMAP(0x03C9, 0xF9) // GREEK SMALL LETTER OMEGA + + // More accented small letters + REMAP(0x03CA, 0xFA) // GREEK SMALL LETTER IOTA WITH DIALYTIKA + REMAP(0x03CB, 0xFB) // GREEK SMALL LETTER UPSILON WITH DIALYTIKA + REMAP(0x03CC, 0xFC) // GREEK SMALL LETTER OMICRON WITH TONOS + REMAP(0x03CD, 0xFD) // GREEK SMALL LETTER UPSILON WITH TONOS + REMAP(0x03CE, 0xFE) // GREEK SMALL LETTER OMEGA WITH TONOS + } + } + else /*ASCII or Unhandled*/ { if (utf8.length() == 1) return utf8.at(0); diff --git a/src/graphics/niche/InkHUD/AppletFont.h b/src/graphics/niche/InkHUD/AppletFont.h index e1fe37974..02ba13c31 100644 --- a/src/graphics/niche/InkHUD/AppletFont.h +++ b/src/graphics/niche/InkHUD/AppletFont.h @@ -26,6 +26,7 @@ class AppletFont WINDOWS_1250, WINDOWS_1251, WINDOWS_1252, + WINDOWS_1253, }; AppletFont(); @@ -84,4 +85,12 @@ class AppletFont #define FREESANS_9PT_WIN1252 InkHUD::AppletFont(FreeSans9pt_Win1252, InkHUD::AppletFont::WINDOWS_1252, -2, -1) #define FREESANS_6PT_WIN1252 InkHUD::AppletFont(FreeSans6pt_Win1252, InkHUD::AppletFont::WINDOWS_1252, -1, -2) +// Greek +#include "graphics/niche/Fonts/FreeSans12pt_Win1253.h" +#include "graphics/niche/Fonts/FreeSans6pt_Win1253.h" +#include "graphics/niche/Fonts/FreeSans9pt_Win1253.h" +#define FREESANS_12PT_WIN1253 InkHUD::AppletFont(FreeSans12pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -3, 1) +#define FREESANS_9PT_WIN1253 InkHUD::AppletFont(FreeSans9pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -2, -1) +#define FREESANS_6PT_WIN1253 InkHUD::AppletFont(FreeSans6pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -1, -2) + #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp index d383a11e4..4cf83966b 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp @@ -4,7 +4,7 @@ using namespace NicheGraphics; -void InkHUD::MapApplet::onRender() +void InkHUD::MapApplet::onRender(bool full) { // Abort if no markers to render if (!enoughMarkers()) { diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h index f45a36071..11dfb39d9 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h @@ -27,7 +27,7 @@ namespace NicheGraphics::InkHUD class MapApplet : public Applet { public: - void onRender() override; + void onRender(bool full) override; protected: virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp index 5c9906fba..9794c3efb 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp @@ -103,7 +103,7 @@ uint8_t InkHUD::NodeListApplet::maxCards() } // Draw, using info which derived applet placed into NodeListApplet::cards for us -void InkHUD::NodeListApplet::onRender() +void InkHUD::NodeListApplet::onRender(bool full) { // ================================ diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h index c2340027b..8babdba03 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h @@ -46,7 +46,7 @@ class NodeListApplet : public Applet, public MeshModule public: NodeListApplet(const char *name); - void onRender() override; + void onRender(bool full) override; bool wantPacket(const meshtastic_MeshPacket *p) override; ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; diff --git a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp index c52719e55..71b6d9a7a 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp @@ -6,7 +6,7 @@ using namespace NicheGraphics; // All drawing happens here // Our basic example doesn't do anything useful. It just passively prints some text. -void InkHUD::BasicExampleApplet::onRender() +void InkHUD::BasicExampleApplet::onRender(bool full) { printAt(0, 0, "Hello, World!"); diff --git a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h index aed63cdc8..a36f6e8d5 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h @@ -28,7 +28,7 @@ class BasicExampleApplet : public Applet // You must have an onRender() method // All drawing happens here - void onRender() override; + void onRender(bool full) override; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp index 6b02f4c92..cf3fd7714 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp @@ -35,7 +35,7 @@ ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_Mesh // We can trigger a render by calling requestUpdate() // Render might be called by some external source // We should always be ready to draw -void InkHUD::NewMsgExampleApplet::onRender() +void InkHUD::NewMsgExampleApplet::onRender(bool full) { printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0) diff --git a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h index 22670a0f0..599f08a7a 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h @@ -34,7 +34,7 @@ class NewMsgExampleApplet : public Applet, public SinglePortModule NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {} // All drawing happens here - void onRender() override; + void onRender(bool full) override; // Your applet might also want to use some of these // Useful for setting up or tidying up diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp new file mode 100644 index 000000000..3afa80149 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp @@ -0,0 +1,199 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./AlignStickApplet.h" + +using namespace NicheGraphics; + +InkHUD::AlignStickApplet::AlignStickApplet() +{ + if (!settings->joystick.aligned) + bringToForeground(); +} + +void InkHUD::AlignStickApplet::onRender(bool full) +{ + setFont(fontMedium); + printAt(0, 0, "Align Joystick:"); + setFont(fontSmall); + std::string instructions = "Move joystick in the direction indicated"; + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), instructions); + + // Size of the region in which the joystick graphic should fit + uint16_t joyXLimit = X(0.8); + uint16_t contentH = fontMedium.lineHeight() * 1.5 + fontSmall.lineHeight() * 1; + if (getTextWidth(instructions) > width()) + contentH += fontSmall.lineHeight(); + uint16_t freeY = height() - contentH - fontSmall.lineHeight() * 1.2; + uint16_t joyYLimit = freeY * 0.8; + + // Use the shorter of the two + uint16_t joyWidth = joyXLimit < joyYLimit ? joyXLimit : joyYLimit; + + // Center the joystick graphic + uint16_t centerX = X(0.5); + uint16_t centerY = contentH + freeY * 0.5; + + // Draw joystick graphic + drawStick(centerX, centerY, joyWidth); + + setFont(fontSmall); + printAt(X(0.5), Y(1.0) - fontSmall.lineHeight() * 0.2, "Long press to skip", CENTER, BOTTOM); +} + +// Draw a scalable joystick graphic +void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uint16_t width) +{ + if (width < 9) // too small to draw + return; + + else if (width < 40) { // only draw up arrow + uint16_t chamfer = width < 20 ? 1 : 2; + + // Draw filled up arrow + drawDirection(centerX, centerY - width / 4, Direction::UP, width, chamfer, BLACK); + + } else { // large enough to draw the full thing + uint16_t chamfer = width < 80 ? 1 : 2; + uint16_t stroke = 3; // pixels + uint16_t arrowW = width * 0.22; + uint16_t hollowW = arrowW - stroke * 2; + + // Draw center circle + fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2), BLACK); + fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2) - stroke, WHITE); + + // Draw filled up arrow + drawDirection(centerX, centerY - width / 2, Direction::UP, arrowW, chamfer, BLACK); + + // Draw down arrow + drawDirection(centerX, centerY + width / 2, Direction::DOWN, arrowW, chamfer, BLACK); + drawDirection(centerX, centerY + width / 2 - stroke, Direction::DOWN, hollowW, 0, WHITE); + + // Draw left arrow + drawDirection(centerX - width / 2, centerY, Direction::LEFT, arrowW, chamfer, BLACK); + drawDirection(centerX - width / 2 + stroke, centerY, Direction::LEFT, hollowW, 0, WHITE); + + // Draw right arrow + drawDirection(centerX + width / 2, centerY, Direction::RIGHT, arrowW, chamfer, BLACK); + drawDirection(centerX + width / 2 - stroke, centerY, Direction::RIGHT, hollowW, 0, WHITE); + } +} + +// Draw a scalable joystick direction arrow +// a right-triangle with blunted tips +/* + _ <--point + ^ / \ + | / \ + size / \ + | / \ + v |_________| + +*/ +void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, + uint16_t chamfer, Color color) +{ + uint16_t chamferW = chamfer * 2 + 1; + uint16_t triangleW = size - chamferW; + + // Draw arrow + switch (direction) { + case Direction::UP: + fillRect(pointX - chamfer, pointY, chamferW, triangleW, color); + fillRect(pointX - chamfer - triangleW, pointY + triangleW, chamferW + triangleW * 2, chamferW, color); + fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY + triangleW, pointX - chamfer, + pointY + triangleW, color); + fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY + triangleW, pointX + chamfer, + pointY + triangleW, color); + break; + case Direction::DOWN: + fillRect(pointX - chamfer, pointY - triangleW + 1, chamferW, triangleW, color); + fillRect(pointX - chamfer - triangleW, pointY - size + 1, chamferW + triangleW * 2, chamferW, color); + fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY - triangleW, pointX - chamfer, + pointY - triangleW, color); + fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY - triangleW, pointX + chamfer, + pointY - triangleW, color); + break; + case Direction::LEFT: + fillRect(pointX, pointY - chamfer, triangleW, chamferW, color); + fillRect(pointX + triangleW, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); + fillTriangle(pointX, pointY - chamfer, pointX + triangleW, pointY - chamfer - triangleW, pointX + triangleW, + pointY - chamfer, color); + fillTriangle(pointX, pointY + chamfer, pointX + triangleW, pointY + chamfer + triangleW, pointX + triangleW, + pointY + chamfer, color); + break; + case Direction::RIGHT: + fillRect(pointX - triangleW + 1, pointY - chamfer, triangleW, chamferW, color); + fillRect(pointX - size + 1, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); + fillTriangle(pointX, pointY - chamfer, pointX - triangleW, pointY - chamfer - triangleW, pointX - triangleW, + pointY - chamfer, color); + fillTriangle(pointX, pointY + chamfer, pointX - triangleW, pointY + chamfer + triangleW, pointX - triangleW, + pointY + chamfer, color); + break; + } +} + +void InkHUD::AlignStickApplet::onForeground() +{ + // Prevent most other applets from requesting update, and skip their rendering entirely + // Another system applet with a higher precedence can potentially ignore this + SystemApplet::lockRendering = true; + SystemApplet::lockRequests = true; + + handleInput = true; // Intercept the button input for our applet +} + +void InkHUD::AlignStickApplet::onBackground() +{ + // Allow normal update behavior to resume + SystemApplet::lockRendering = false; + SystemApplet::lockRequests = false; + SystemApplet::handleInput = false; + + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case + inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); +} + +void InkHUD::AlignStickApplet::onButtonLongPress() +{ + sendToBackground(); +} + +void InkHUD::AlignStickApplet::onExitLong() +{ + sendToBackground(); +} + +void InkHUD::AlignStickApplet::onNavUp() +{ + settings->joystick.aligned = true; + + sendToBackground(); +} + +void InkHUD::AlignStickApplet::onNavDown() +{ + inkhud->rotateJoystick(2); // 180 deg + settings->joystick.aligned = true; + + sendToBackground(); +} + +void InkHUD::AlignStickApplet::onNavLeft() +{ + inkhud->rotateJoystick(3); // 270 deg + settings->joystick.aligned = true; + + sendToBackground(); +} + +void InkHUD::AlignStickApplet::onNavRight() +{ + inkhud->rotateJoystick(1); // 90 deg + settings->joystick.aligned = true; + + sendToBackground(); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h new file mode 100644 index 000000000..7c8d00155 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h @@ -0,0 +1,50 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +System Applet for manually aligning the joystick with the screen + +should be run at startup if the joystick is enabled +and not aligned to the screen + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/SystemApplet.h" + +namespace NicheGraphics::InkHUD +{ + +class AlignStickApplet : public SystemApplet +{ + public: + AlignStickApplet(); + + void onRender(bool full) override; + void onForeground() override; + void onBackground() override; + void onButtonLongPress() override; + void onExitLong() override; + void onNavUp() override; + void onNavDown() override; + void onNavLeft() override; + void onNavRight() override; + + protected: + enum Direction { + UP, + DOWN, + LEFT, + RIGHT, + }; + + void drawStick(uint16_t centerX, uint16_t centerY, uint16_t width); + void drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color); +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp index 4f99d99ee..0cc6f50ed 100644 --- a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp @@ -6,6 +6,8 @@ using namespace NicheGraphics; InkHUD::BatteryIconApplet::BatteryIconApplet() { + alwaysRender = true; // render everytime the screen is updated + // Show at boot, if user has previously enabled the feature if (settings->optionalFeatures.batteryIcon) bringToForeground(); @@ -44,7 +46,7 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta return 0; // Tell Observable to continue informing other observers } -void InkHUD::BatteryIconApplet::onRender() +void InkHUD::BatteryIconApplet::onRender(bool full) { // Fill entire tile // - size of icon controlled by size of tile diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h index e5b4172be..ceaf88d7f 100644 --- a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h @@ -23,7 +23,7 @@ class BatteryIconApplet : public SystemApplet public: BatteryIconApplet(); - void onRender() override; + void onRender(bool full) override; int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available private: diff --git a/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.cpp new file mode 100644 index 000000000..57581d56b --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.cpp @@ -0,0 +1,257 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD +#include "./KeyboardApplet.h" + +using namespace NicheGraphics; + +InkHUD::KeyboardApplet::KeyboardApplet() +{ + // Calculate row widths + for (uint8_t row = 0; row < KBD_ROWS; row++) { + rowWidths[row] = 0; + for (uint8_t col = 0; col < KBD_COLS; col++) + rowWidths[row] += keyWidths[row * KBD_COLS + col]; + } +} + +void InkHUD::KeyboardApplet::onRender(bool full) +{ + uint16_t em = fontSmall.lineHeight(); // 16 pt + uint16_t keyH = Y(1.0) / KBD_ROWS; + int16_t keyTopPadding = (keyH - fontSmall.lineHeight()) / 2; + + if (full) { // Draw full keyboard + for (uint8_t row = 0; row < KBD_ROWS; row++) { + + // Calculate the remaining space to be used as padding + int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4); + + // Draw keys + uint16_t xPos = 0; + for (uint8_t col = 0; col < KBD_COLS; col++) { + Color fgcolor = BLACK; + uint8_t index = row * KBD_COLS + col; + uint16_t keyX = ((xPos * em) >> 4) + ((col * keyXPadding) / (KBD_COLS - 1)); + uint16_t keyY = row * keyH; + uint16_t keyW = (keyWidths[index] * em) >> 4; + if (index == selectedKey) { + fgcolor = WHITE; + fillRect(keyX, keyY, keyW, keyH, BLACK); + } + drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[index], fgcolor); + xPos += keyWidths[index]; + } + } + } else { // Only draw the difference + if (selectedKey != prevSelectedKey) { + // Draw previously selected key + uint8_t row = prevSelectedKey / KBD_COLS; + int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4); + uint16_t xPos = 0; + for (uint8_t i = prevSelectedKey - (prevSelectedKey % KBD_COLS); i < prevSelectedKey; i++) + xPos += keyWidths[i]; + uint16_t keyX = ((xPos * em) >> 4) + (((prevSelectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1)); + uint16_t keyY = row * keyH; + uint16_t keyW = (keyWidths[prevSelectedKey] * em) >> 4; + fillRect(keyX, keyY, keyW, keyH, WHITE); + drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[prevSelectedKey], BLACK); + + // Draw newly selected key + row = selectedKey / KBD_COLS; + keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4); + xPos = 0; + for (uint8_t i = selectedKey - (selectedKey % KBD_COLS); i < selectedKey; i++) + xPos += keyWidths[i]; + keyX = ((xPos * em) >> 4) + (((selectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1)); + keyY = row * keyH; + keyW = (keyWidths[selectedKey] * em) >> 4; + fillRect(keyX, keyY, keyW, keyH, BLACK); + drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[selectedKey], WHITE); + } + } + + prevSelectedKey = selectedKey; +} + +// Draw the key label corresponding to the char +// for most keys it draws the character itself +// for ['\b', '\n', ' ', '\x1b'] it draws special glyphs +void InkHUD::KeyboardApplet::drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color) +{ + if (key == '\b') { + // Draw backspace glyph: 13 x 9 px + /** + * [][][][][][][][][] + * [][] [] + * [][] [] [] [] + * [][] [] [] [] + * [][] [] [] + * [][] [] [] [] + * [][] [] [] [] + * [][] [] + * [][][][][][][][][] + */ + const uint8_t bsBitmap[] = {0x0f, 0xf8, 0x18, 0x08, 0x32, 0x28, 0x61, 0x48, 0xc0, + 0x88, 0x61, 0x48, 0x32, 0x28, 0x18, 0x08, 0x0f, 0xf8}; + uint16_t leftPadding = (width - 13) >> 1; + drawBitmap(left + leftPadding, top + 1, bsBitmap, 13, 9, color); + } else if (key == '\n') { + // Draw done glyph: 12 x 9 px + /** + * [][] + * [][] + * [][] + * [][] + * [][] + * [][] [][] + * [][] [][] + * [][][] + * [] + */ + const uint8_t doneBitmap[] = {0x00, 0x30, 0x00, 0x60, 0x00, 0xc0, 0x01, 0x80, 0x03, + 0x00, 0xc6, 0x00, 0x6c, 0x00, 0x38, 0x00, 0x10, 0x00}; + uint16_t leftPadding = (width - 12) >> 1; + drawBitmap(left + leftPadding, top + 1, doneBitmap, 12, 9, color); + } else if (key == ' ') { + // Draw space glyph: 13 x 9 px + /** + * + * + * + * + * [] [] + * [] [] + * [][][][][][][][][][][][][] + * + * + */ + const uint8_t spaceBitmap[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, + 0x08, 0x80, 0x08, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00}; + uint16_t leftPadding = (width - 13) >> 1; + drawBitmap(left + leftPadding, top + 1, spaceBitmap, 13, 9, color); + } else if (key == '\x1b') { + setTextColor(color); + std::string keyText = "ESC"; + uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1; + printAt(left + leftPadding, top, keyText); + } else { + setTextColor(color); + if (key >= 0x61) + key -= 32; // capitalize + std::string keyText = std::string(1, key); + uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1; + printAt(left + leftPadding, top, keyText); + } +} + +void InkHUD::KeyboardApplet::onForeground() +{ + handleInput = true; // Intercept the button input for our applet + + // Select the first key + selectedKey = 0; + prevSelectedKey = 0; +} + +void InkHUD::KeyboardApplet::onBackground() +{ + handleInput = false; +} + +void InkHUD::KeyboardApplet::onButtonShortPress() +{ + char key = keys[selectedKey]; + if (key == '\n') { + inkhud->freeTextDone(); + inkhud->closeKeyboard(); + } else if (key == '\x1b') { + inkhud->freeTextCancel(); + inkhud->closeKeyboard(); + } else { + inkhud->freeText(key); + } +} + +void InkHUD::KeyboardApplet::onButtonLongPress() +{ + char key = keys[selectedKey]; + if (key == '\n') { + inkhud->freeTextDone(); + inkhud->closeKeyboard(); + } else if (key == '\x1b') { + inkhud->freeTextCancel(); + inkhud->closeKeyboard(); + } else { + if (key >= 0x61) + key -= 32; // capitalize + inkhud->freeText(key); + } +} + +void InkHUD::KeyboardApplet::onExitShort() +{ + inkhud->freeTextCancel(); + inkhud->closeKeyboard(); +} + +void InkHUD::KeyboardApplet::onExitLong() +{ + inkhud->freeTextCancel(); + inkhud->closeKeyboard(); +} + +void InkHUD::KeyboardApplet::onNavUp() +{ + if (selectedKey < KBD_COLS) // wrap + selectedKey += KBD_COLS * (KBD_ROWS - 1); + else // move 1 row back + selectedKey -= KBD_COLS; + + // Request rendering over the previously drawn render + requestUpdate(EInk::UpdateTypes::FAST, false); + // Force an update to bypass lockRequests + inkhud->forceUpdate(EInk::UpdateTypes::FAST); +} + +void InkHUD::KeyboardApplet::onNavDown() +{ + selectedKey += KBD_COLS; + selectedKey %= (KBD_COLS * KBD_ROWS); + + // Request rendering over the previously drawn render + requestUpdate(EInk::UpdateTypes::FAST, false); + // Force an update to bypass lockRequests + inkhud->forceUpdate(EInk::UpdateTypes::FAST); +} + +void InkHUD::KeyboardApplet::onNavLeft() +{ + if (selectedKey % KBD_COLS == 0) // wrap + selectedKey += KBD_COLS - 1; + else // move 1 column back + selectedKey--; + + // Request rendering over the previously drawn render + requestUpdate(EInk::UpdateTypes::FAST, false); + // Force an update to bypass lockRequests + inkhud->forceUpdate(EInk::UpdateTypes::FAST); +} + +void InkHUD::KeyboardApplet::onNavRight() +{ + if (selectedKey % KBD_COLS == KBD_COLS - 1) // wrap + selectedKey -= KBD_COLS - 1; + else // move 1 column forward + selectedKey++; + + // Request rendering over the previously drawn render + requestUpdate(EInk::UpdateTypes::FAST, false); + // Force an update to bypass lockRequests + inkhud->forceUpdate(EInk::UpdateTypes::FAST); +} + +uint16_t InkHUD::KeyboardApplet::getKeyboardHeight() +{ + const uint16_t keyH = fontSmall.lineHeight() * 1.2; + return keyH * KBD_ROWS; +} +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h new file mode 100644 index 000000000..306a8d8e3 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h @@ -0,0 +1,66 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +System Applet to render an on-screeen keyboard + +*/ + +#pragma once + +#include "configuration.h" +#include "graphics/niche/InkHUD/InkHUD.h" +#include "graphics/niche/InkHUD/SystemApplet.h" +#include +namespace NicheGraphics::InkHUD +{ + +class KeyboardApplet : public SystemApplet +{ + public: + KeyboardApplet(); + + void onRender(bool full) override; + void onForeground() override; + void onBackground() override; + void onButtonShortPress() override; + void onButtonLongPress() override; + void onExitShort() override; + void onExitLong() override; + void onNavUp() override; + void onNavDown() override; + void onNavLeft() override; + void onNavRight() override; + + static uint16_t getKeyboardHeight(); // used to set the keyboard tile height + + private: + void drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color); + + static const uint8_t KBD_COLS = 11; + static const uint8_t KBD_ROWS = 4; + + const char keys[KBD_COLS * KBD_ROWS] = { + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b', // row 0 + 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n', // row 1 + 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '!', ' ', // row 2 + 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '?', '\x1b' // row 3 + }; + + // This array represents the widths of each key in points + // 16 pt = line height of the text + const uint16_t keyWidths[KBD_COLS * KBD_ROWS] = { + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 0 + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 1 + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 2 + 16, 16, 16, 16, 16, 16, 16, 10, 10, 12, 40 // row 3 + }; + + uint16_t rowWidths[KBD_ROWS]; + uint8_t selectedKey = 0; // selected key index + uint8_t prevSelectedKey = 0; +}; + +} // namespace NicheGraphics::InkHUD + +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp index ecaa7cea3..b2c58fc60 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp @@ -30,7 +30,7 @@ InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet") // This is then drawn with a FULL refresh by Renderer::begin } -void InkHUD::LogoApplet::onRender() +void InkHUD::LogoApplet::onRender(bool full) { // Size of the region which the logo should "scale to fit" uint16_t logoWLimit = X(0.8); @@ -120,7 +120,7 @@ void InkHUD::LogoApplet::onBackground() // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); } // Begin displaying the screen which is shown at shutdown @@ -138,10 +138,10 @@ void InkHUD::LogoApplet::onShutdown() // Intention is to restore display health. inverted = true; - inkhud->forceUpdate(Drivers::EInk::FULL, false); + inkhud->forceUpdate(Drivers::EInk::FULL, true, false); delay(1000); // Cooldown. Back to back updates aren't great for health. inverted = false; - inkhud->forceUpdate(Drivers::EInk::FULL, false); + inkhud->forceUpdate(Drivers::EInk::FULL, true, false); delay(1000); // Cooldown // Prepare for the powered-off screen now @@ -155,6 +155,18 @@ void InkHUD::LogoApplet::onShutdown() // This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is complete } +void InkHUD::LogoApplet::onApplyingChanges() +{ + bringToForeground(); + + textLeft = ""; + textRight = ""; + textTitle = "Applying changes"; + fontTitle = fontSmall; + + inkhud->forceUpdate(Drivers::EInk::FAST, false); +} + void InkHUD::LogoApplet::onReboot() { bringToForeground(); @@ -164,7 +176,7 @@ void InkHUD::LogoApplet::onReboot() textTitle = "Rebooting..."; fontTitle = fontSmall; - inkhud->forceUpdate(Drivers::EInk::FULL, false); + inkhud->forceUpdate(Drivers::EInk::FULL, true, false); // Perform the update right now, waiting here until complete } diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h index 3f604baed..d70dcc7b2 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h @@ -21,11 +21,12 @@ class LogoApplet : public SystemApplet, public concurrency::OSThread { public: LogoApplet(); - void onRender() override; + void onRender(bool full) override; void onForeground() override; void onBackground() override; void onShutdown() override; void onReboot() override; + void onApplyingChanges(); protected: int32_t runOnce() override; diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h index c84ee09e0..7ec76292b 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h @@ -19,6 +19,7 @@ namespace NicheGraphics::InkHUD enum MenuAction { NO_ACTION, SEND_PING, + FREE_TEXT, STORE_CANNEDMESSAGE_SELECTION, SEND_CANNEDMESSAGE, SHUTDOWN, @@ -30,11 +31,90 @@ enum MenuAction { TOGGLE_AUTOSHOW_APPLET, SET_RECENTS, ROTATE, + ALIGN_JOYSTICK, LAYOUT, TOGGLE_BATTERY_ICON, TOGGLE_NOTIFICATIONS, TOGGLE_INVERT_COLOR, TOGGLE_12H_CLOCK, + // Regions + SET_REGION_US, + SET_REGION_EU_868, + SET_REGION_EU_433, + SET_REGION_CN, + SET_REGION_JP, + SET_REGION_ANZ, + SET_REGION_KR, + SET_REGION_TW, + SET_REGION_RU, + SET_REGION_IN, + SET_REGION_NZ_865, + SET_REGION_TH, + SET_REGION_LORA_24, + SET_REGION_UA_433, + SET_REGION_UA_868, + SET_REGION_MY_433, + SET_REGION_MY_919, + SET_REGION_SG_923, + SET_REGION_PH_433, + SET_REGION_PH_868, + SET_REGION_PH_915, + SET_REGION_ANZ_433, + SET_REGION_KZ_433, + SET_REGION_KZ_863, + SET_REGION_NP_865, + SET_REGION_BR_902, + // Device Roles + SET_ROLE_CLIENT, + SET_ROLE_CLIENT_MUTE, + SET_ROLE_ROUTER, + SET_ROLE_REPEATER, + // Presets + SET_PRESET_LONG_SLOW, + SET_PRESET_LONG_MODERATE, + SET_PRESET_LONG_FAST, + SET_PRESET_MEDIUM_SLOW, + SET_PRESET_MEDIUM_FAST, + SET_PRESET_SHORT_SLOW, + SET_PRESET_SHORT_FAST, + SET_PRESET_SHORT_TURBO, + // Timezones + SET_TZ_US_HAWAII, + SET_TZ_US_ALASKA, + SET_TZ_US_PACIFIC, + SET_TZ_US_ARIZONA, + SET_TZ_US_MOUNTAIN, + SET_TZ_US_CENTRAL, + SET_TZ_US_EASTERN, + SET_TZ_BR_BRAZILIA, + SET_TZ_UTC, + SET_TZ_EU_WESTERN, + SET_TZ_EU_CENTRAL, + SET_TZ_EU_EASTERN, + SET_TZ_ASIA_KOLKATA, + SET_TZ_ASIA_HONG_KONG, + SET_TZ_AU_AWST, + SET_TZ_AU_ACST, + SET_TZ_AU_AEST, + SET_TZ_PACIFIC_NZ, + // Power + TOGGLE_POWER_SAVE, + CALIBRATE_ADC, + // Bluetooth + TOGGLE_BLUETOOTH, + TOGGLE_BLUETOOTH_PAIR_MODE, + // Channel + TOGGLE_CHANNEL_UPLINK, + TOGGLE_CHANNEL_DOWNLINK, + TOGGLE_CHANNEL_POSITION, + SET_CHANNEL_PRECISION, + // Display + TOGGLE_DISPLAY_UNITS, + // Network + TOGGLE_WIFI, + // Administration + RESET_NODEDB_ALL, + RESET_NODEDB_KEEP_FAVORITES, }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 09f76ed46..6a141f73e 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -2,16 +2,21 @@ #include "./MenuApplet.h" -#include "RTC.h" - +#include "DisplayFormatters.h" +#include "GPS.h" #include "MeshService.h" +#include "RTC.h" #include "Router.h" #include "airtime.h" #include "main.h" +#include "mesh/generated/meshtastic/deviceonly.pb.h" #include "power.h" - -#if !MESHTASTIC_EXCLUDE_GPS -#include "GPS.h" +#include +#include +#if defined(ARCH_ESP32) && HAS_WIFI +#include "mesh/wifi/WiFiAPClient.h" +#include +#include #endif using namespace NicheGraphics; @@ -22,6 +27,18 @@ static constexpr uint8_t MENU_TIMEOUT_SEC = 60; // How many seconds before menu // These are offered to users as possible values for settings.recentlyActiveSeconds static constexpr uint8_t RECENTS_OPTIONS_MINUTES[] = {2, 5, 10, 30, 60, 120}; +struct PositionPrecisionOption { + uint8_t value; // proto value + const char *metric; + const char *imperial; +}; + +static constexpr PositionPrecisionOption POSITION_PRECISION_OPTIONS[] = { + {32, "Precise", "Precise"}, {19, "50 m", "150 ft"}, {18, "90 m", "300 ft"}, {17, "200 m", "600 ft"}, + {16, "350 m", "0.2 mi"}, {15, "700 m", "0.5 mi"}, {14, "1.5 km", "0.9 mi"}, {13, "2.9 km", "1.8 mi"}, + {12, "5.8 km", "3.6 mi"}, {11, "12 km", "7.3 mi"}, {10, "23 km", "15 mi"}, +}; + InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet") { // No timer tasks at boot @@ -45,8 +62,15 @@ void InkHUD::MenuApplet::onForeground() // We do need this before we render, but we can optimize by just calculating it once now systemInfoPanelHeight = getSystemInfoPanelHeight(); - // Display initial menu page - showPage(MenuPage::ROOT); + // Force Region page ONLY when explicitly requested (one-shot) + if (inkhud->forceRegionMenu) { + + inkhud->forceRegionMenu = false; // consume one-shot flag + showPage(MenuPage::REGION); + + } else { + showPage(MenuPage::ROOT); + } // If device has a backlight which isn't controlled by aux button: // backlight on always when menu opens. @@ -66,6 +90,8 @@ void InkHUD::MenuApplet::onForeground() OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); OSThread::enabled = true; + freeTextMode = false; + // Upgrade the refresh to FAST, for guaranteed responsiveness inkhud->forceUpdate(EInk::UpdateTypes::FAST); } @@ -92,6 +118,8 @@ void InkHUD::MenuApplet::onBackground() SystemApplet::lockRequests = false; SystemApplet::handleInput = false; + handleFreeText = false; + // Restore the user applet whose tile we borrowed if (borrowedTileOwner) borrowedTileOwner->bringToForeground(); @@ -139,6 +167,150 @@ int32_t InkHUD::MenuApplet::runOnce() return OSThread::disable(); } +static void applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode region) +{ + if (config.lora.region == region) + return; + + config.lora.region = region; + + auto changes = SEGMENT_CONFIG; + +#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) + 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 { + 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); + } + } +#endif + + config.lora.tx_enabled = true; + + initRegion(); + + if (myRegion && myRegion->dutyCycle < 100) { + config.lora.ignore_mqtt = true; + } + + if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { + sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); + changes |= SEGMENT_MODULECONFIG; + } + // Notify UI that changes are being applied + InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); + service->reloadConfig(changes); + + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; +} + +static void applyDeviceRole(meshtastic_Config_DeviceConfig_Role role) +{ + if (config.device.role == role) + return; + + config.device.role = role; + + nodeDB->saveToDisk(SEGMENT_CONFIG); + + service->reloadConfig(SEGMENT_CONFIG); + + // Notify UI that changes are being applied + InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); + + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; +} + +static void applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset preset) +{ + if (config.lora.modem_preset == preset) + return; + + config.lora.use_preset = true; + config.lora.modem_preset = preset; + + nodeDB->saveToDisk(SEGMENT_CONFIG); + service->reloadConfig(SEGMENT_CONFIG); + + // Notify UI that changes are being applied + InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); + + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; +} + +static const char *getTimezoneLabelFromValue(const char *tzdef) +{ + if (!tzdef || !*tzdef) + return "Unset"; + + // Must match TIMEZONE menu entries + if (strcmp(tzdef, "HST10") == 0) + return "US/Hawaii"; + if (strcmp(tzdef, "AKST9AKDT,M3.2.0,M11.1.0") == 0) + return "US/Alaska"; + if (strcmp(tzdef, "PST8PDT,M3.2.0,M11.1.0") == 0) + return "US/Pacific"; + if (strcmp(tzdef, "MST7") == 0) + return "US/Arizona"; + if (strcmp(tzdef, "MST7MDT,M3.2.0,M11.1.0") == 0) + return "US/Mountain"; + if (strcmp(tzdef, "CST6CDT,M3.2.0,M11.1.0") == 0) + return "US/Central"; + if (strcmp(tzdef, "EST5EDT,M3.2.0,M11.1.0") == 0) + return "US/Eastern"; + if (strcmp(tzdef, "BRT3") == 0) + return "BR/Brasilia"; + if (strcmp(tzdef, "UTC0") == 0) + return "UTC"; + if (strcmp(tzdef, "GMT0BST,M3.5.0/1,M10.5.0") == 0) + return "EU/Western"; + if (strcmp(tzdef, "CET-1CEST,M3.5.0,M10.5.0/3") == 0) + return "EU/Central"; + if (strcmp(tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4") == 0) + return "EU/Eastern"; + if (strcmp(tzdef, "IST-5:30") == 0) + return "Asia/Kolkata"; + if (strcmp(tzdef, "HKT-8") == 0) + return "Asia/Hong Kong"; + if (strcmp(tzdef, "AWST-8") == 0) + return "AU/AWST"; + if (strcmp(tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3") == 0) + return "AU/ACST"; + if (strcmp(tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3") == 0) + return "AU/AEST"; + if (strcmp(tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3") == 0) + return "Pacific/NZ"; + + return tzdef; // fallback for unknown/custom values +} + +static void applyTimezone(const char *tz) +{ + if (!tz || strcmp(config.device.tzdef, tz) == 0) + return; + + strncpy(config.device.tzdef, tz, sizeof(config.device.tzdef)); + config.device.tzdef[sizeof(config.device.tzdef) - 1] = '\0'; + + setenv("TZ", config.device.tzdef, 1); + + nodeDB->saveToDisk(SEGMENT_CONFIG); + service->reloadConfig(SEGMENT_CONFIG); +} + // Perform action for a menu item, then change page // Behaviors for MenuActions are defined here void InkHUD::MenuApplet::execute(MenuItem item) @@ -150,10 +322,18 @@ void InkHUD::MenuApplet::execute(MenuItem item) // Open a submenu without performing any action // Also handles exit case NO_ACTION: + if (currentPage == MenuPage::NODE_CONFIG_CHANNELS && item.nextPage == MenuPage::NODE_CONFIG_CHANNEL_DETAIL) { + + // cursor - 1 because index 0 is "Back" + selectedChannelIndex = cursor - 1; + } break; case NEXT_TILE: inkhud->nextTile(); + // Unselect menu item after tile change + cursorShown = false; + cursor = 0; break; case SEND_PING: @@ -164,12 +344,26 @@ void InkHUD::MenuApplet::execute(MenuItem item) inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); break; + case FREE_TEXT: + OSThread::enabled = false; + handleFreeText = true; + cm.freeTextItem.rawText.erase(); // clear the previous freetext message + freeTextMode = true; // render input field instead of normal menu + // Open the on-screen keyboard if the joystick is enabled + if (settings->joystick.enabled) + inkhud->openKeyboard(); + break; + case STORE_CANNEDMESSAGE_SELECTION: - cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry + if (!settings->joystick.enabled) + cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry + else + cm.selectedMessageItem = &cm.messageItems.at(cursor - 2); // Minus two: offset for the "Send Ping" and free text entry break; case SEND_CANNEDMESSAGE: cm.selectedRecipientItem = &cm.recipientItems.at(cursor); + // send selected message 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; @@ -178,6 +372,10 @@ void InkHUD::MenuApplet::execute(MenuItem item) inkhud->rotate(); break; + case ALIGN_JOYSTICK: + inkhud->openAlignStick(); + break; + case LAYOUT: // Todo: smarter incrementing of tile count settings->userTiles.count++; @@ -192,17 +390,23 @@ void InkHUD::MenuApplet::execute(MenuItem item) break; case TOGGLE_APPLET: - settings->userApplets.active[cursor] = !settings->userApplets.active[cursor]; - inkhud->updateAppletSelection(); + if (item.checkState) { + *item.checkState = !(*item.checkState); + inkhud->updateAppletSelection(); + } break; case TOGGLE_AUTOSHOW_APPLET: // Toggle settings.userApplets.autoshow[] value, via MenuItem::checkState pointer set in populateAutoshowPage() - *items.at(cursor).checkState = !(*items.at(cursor).checkState); + if (item.checkState) { + *item.checkState = !(*item.checkState); + } break; case TOGGLE_NOTIFICATIONS: - settings->optionalFeatures.notifications = !settings->optionalFeatures.notifications; + if (item.checkState) { + *item.checkState = !(*item.checkState); + } break; case TOGGLE_INVERT_COLOR: @@ -214,12 +418,14 @@ void InkHUD::MenuApplet::execute(MenuItem item) nodeDB->saveToDisk(SEGMENT_CONFIG); break; - case SET_RECENTS: - // Set value of settings.recentlyActiveSeconds - // Uses menu cursor to read RECENTS_OPTIONS_MINUTES array (defined at top of this file) - assert(cursor < sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0])); - settings->recentlyActiveSeconds = RECENTS_OPTIONS_MINUTES[cursor] * 60; // Menu items are in minutes + case SET_RECENTS: { + // cursor - 1 because index 0 is "Back" + const uint8_t index = cursor - 1; + constexpr uint8_t optionCount = sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]); + assert(index < optionCount); + settings->recentlyActiveSeconds = RECENTS_OPTIONS_MINUTES[index] * 60; break; + } case SHUTDOWN: LOG_INFO("Shutting down from menu"); @@ -247,8 +453,18 @@ void InkHUD::MenuApplet::execute(MenuItem item) break; case TOGGLE_GPS: - gps->toggleGpsMode(); +#if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; + } else if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; + } else { + // NOT_PRESENT do nothing + break; + } nodeDB->saveToDisk(SEGMENT_CONFIG); + service->reloadConfig(SEGMENT_CONFIG); +#endif break; case ENABLE_BLUETOOTH: @@ -256,10 +472,397 @@ void InkHUD::MenuApplet::execute(MenuItem item) LOG_INFO("Enabling Bluetooth"); config.network.wifi_enabled = false; config.bluetooth.enabled = true; - nodeDB->saveToDisk(); + nodeDB->saveToDisk(SEGMENT_CONFIG); + InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); rebootAtMsec = millis() + 2000; break; + // Power / Network (ESP32-only) +#if defined(ARCH_ESP32) + case TOGGLE_POWER_SAVE: + config.power.is_power_saving = !config.power.is_power_saving; + nodeDB->saveToDisk(SEGMENT_CONFIG); + InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + break; + + case TOGGLE_WIFI: + config.network.wifi_enabled = !config.network.wifi_enabled; + + if (config.network.wifi_enabled) { + // Switch behavior: WiFi ON forces Bluetooth OFF + config.bluetooth.enabled = false; + } + + nodeDB->saveToDisk(SEGMENT_CONFIG); + InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + break; +#endif + // ADC Calibration + case CALIBRATE_ADC: { + // Read current measured voltage + float measuredV = powerStatus->getBatteryVoltageMv() / 1000.0f; + + // Sanity check + if (measuredV < 3.0f || measuredV > 4.5f) { + LOG_WARN("ADC calibration aborted, unreasonable voltage: %.2fV", measuredV); + break; + } + + // Determine the base multiplier currently in effect + float baseMult = 0.0f; + + if (config.power.adc_multiplier_override > 0.0f) { + baseMult = config.power.adc_multiplier_override; + } +#ifdef ADC_MULTIPLIER + else { + baseMult = ADC_MULTIPLIER; + } +#endif + + if (baseMult <= 0.0f) { + LOG_WARN("ADC calibration failed: no base multiplier"); + break; + } + + // Target voltage considered 100% by UI + constexpr float TARGET_VOLTAGE = 4.19f; + + // Calculate new multiplier + float newMult = baseMult * (TARGET_VOLTAGE / measuredV); + + config.power.adc_multiplier_override = newMult; + + nodeDB->saveToDisk(SEGMENT_CONFIG); + + LOG_INFO("ADC calibrated: measured=%.3fV base=%.4f new=%.4f", measuredV, baseMult, newMult); + + break; + } + + // Display + case TOGGLE_DISPLAY_UNITS: + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_METRIC; + else + config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL; + + nodeDB->saveToDisk(SEGMENT_CONFIG); + break; + + // Bluetooth + case TOGGLE_BLUETOOTH: + config.bluetooth.enabled = !config.bluetooth.enabled; + + if (config.bluetooth.enabled) { + // Switch behavior: Bluetooth ON forces WiFi OFF + config.network.wifi_enabled = false; + } + + nodeDB->saveToDisk(SEGMENT_CONFIG); + InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + break; + + case TOGGLE_BLUETOOTH_PAIR_MODE: + config.bluetooth.fixed_pin = !config.bluetooth.fixed_pin; + nodeDB->saveToDisk(SEGMENT_CONFIG); + break; + + // Regions + case SET_REGION_US: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + break; + + case SET_REGION_EU_868: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + break; + + case SET_REGION_EU_433: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_433); + break; + + case SET_REGION_CN: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_CN); + break; + + case SET_REGION_JP: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_JP); + break; + + case SET_REGION_ANZ: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ANZ); + break; + case SET_REGION_KR: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_KR); + break; + + case SET_REGION_TW: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_TW); + break; + + case SET_REGION_RU: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_RU); + break; + + case SET_REGION_IN: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_IN); + break; + + case SET_REGION_NZ_865: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_NZ_865); + break; + + case SET_REGION_TH: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_TH); + break; + + case SET_REGION_LORA_24: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); + break; + + case SET_REGION_UA_433: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_UA_433); + break; + + case SET_REGION_UA_868: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_UA_868); + break; + + case SET_REGION_MY_433: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_MY_433); + break; + + case SET_REGION_MY_919: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_MY_919); + break; + + case SET_REGION_SG_923: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_SG_923); + break; + + case SET_REGION_PH_433: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_PH_433); + break; + + case SET_REGION_PH_868: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_PH_868); + break; + + case SET_REGION_PH_915: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_PH_915); + break; + + case SET_REGION_ANZ_433: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ANZ_433); + break; + + case SET_REGION_KZ_433: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_KZ_433); + break; + + case SET_REGION_KZ_863: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_KZ_863); + break; + + case SET_REGION_NP_865: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_NP_865); + break; + + case SET_REGION_BR_902: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_BR_902); + break; + + // Roles + case SET_ROLE_CLIENT: + applyDeviceRole(meshtastic_Config_DeviceConfig_Role_CLIENT); + break; + + case SET_ROLE_CLIENT_MUTE: + applyDeviceRole(meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE); + break; + + case SET_ROLE_ROUTER: + applyDeviceRole(meshtastic_Config_DeviceConfig_Role_ROUTER); + break; + + case SET_ROLE_REPEATER: + applyDeviceRole(meshtastic_Config_DeviceConfig_Role_REPEATER); + break; + + // Presets + case SET_PRESET_LONG_SLOW: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW); + break; + + case SET_PRESET_LONG_MODERATE: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE); + break; + + case SET_PRESET_LONG_FAST: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST); + break; + + case SET_PRESET_MEDIUM_SLOW: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW); + break; + + case SET_PRESET_MEDIUM_FAST: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST); + break; + + case SET_PRESET_SHORT_SLOW: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW); + break; + + case SET_PRESET_SHORT_FAST: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST); + break; + + case SET_PRESET_SHORT_TURBO: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO); + break; + + // Timezones + case SET_TZ_US_HAWAII: + applyTimezone("HST10"); + break; + + case SET_TZ_US_ALASKA: + applyTimezone("AKST9AKDT,M3.2.0,M11.1.0"); + break; + + case SET_TZ_US_PACIFIC: + applyTimezone("PST8PDT,M3.2.0,M11.1.0"); + break; + + case SET_TZ_US_ARIZONA: + applyTimezone("MST7"); + break; + + case SET_TZ_US_MOUNTAIN: + applyTimezone("MST7MDT,M3.2.0,M11.1.0"); + break; + + case SET_TZ_US_CENTRAL: + applyTimezone("CST6CDT,M3.2.0,M11.1.0"); + break; + + case SET_TZ_US_EASTERN: + applyTimezone("EST5EDT,M3.2.0,M11.1.0"); + break; + + case SET_TZ_BR_BRAZILIA: + applyTimezone("BRT3"); + break; + + case SET_TZ_UTC: + applyTimezone("UTC0"); + break; + + case SET_TZ_EU_WESTERN: + applyTimezone("GMT0BST,M3.5.0/1,M10.5.0"); + break; + + case SET_TZ_EU_CENTRAL: + applyTimezone("CET-1CEST,M3.5.0,M10.5.0/3"); + break; + + case SET_TZ_EU_EASTERN: + applyTimezone("EET-2EEST,M3.5.0/3,M10.5.0/4"); + break; + + case SET_TZ_ASIA_KOLKATA: + applyTimezone("IST-5:30"); + break; + + case SET_TZ_ASIA_HONG_KONG: + applyTimezone("HKT-8"); + break; + + case SET_TZ_AU_AWST: + applyTimezone("AWST-8"); + break; + + case SET_TZ_AU_ACST: + applyTimezone("ACST-9:30ACDT,M10.1.0,M4.1.0/3"); + break; + + case SET_TZ_AU_AEST: + applyTimezone("AEST-10AEDT,M10.1.0,M4.1.0/3"); + break; + + case SET_TZ_PACIFIC_NZ: + applyTimezone("NZST-12NZDT,M9.5.0,M4.1.0/3"); + break; + + // Channels + case TOGGLE_CHANNEL_UPLINK: { + auto &ch = channels.getByIndex(selectedChannelIndex); + ch.settings.uplink_enabled = !ch.settings.uplink_enabled; + nodeDB->saveToDisk(SEGMENT_CHANNELS); + service->reloadConfig(SEGMENT_CHANNELS); + break; + } + + case TOGGLE_CHANNEL_DOWNLINK: { + auto &ch = channels.getByIndex(selectedChannelIndex); + ch.settings.downlink_enabled = !ch.settings.downlink_enabled; + nodeDB->saveToDisk(SEGMENT_CHANNELS); + service->reloadConfig(SEGMENT_CHANNELS); + break; + } + + case TOGGLE_CHANNEL_POSITION: { + auto &ch = channels.getByIndex(selectedChannelIndex); + + if (!ch.settings.has_module_settings) + ch.settings.has_module_settings = true; + + if (ch.settings.module_settings.position_precision > 0) + ch.settings.module_settings.position_precision = 0; + else + ch.settings.module_settings.position_precision = 13; // default + + nodeDB->saveToDisk(SEGMENT_CHANNELS); + service->reloadConfig(SEGMENT_CHANNELS); + break; + } + + case SET_CHANNEL_PRECISION: { + auto &ch = channels.getByIndex(selectedChannelIndex); + + if (!ch.settings.has_module_settings) + ch.settings.has_module_settings = true; + + // Cursor - 1 because of "Back" + uint8_t index = cursor - 1; + + constexpr uint8_t optionCount = sizeof(POSITION_PRECISION_OPTIONS) / sizeof(POSITION_PRECISION_OPTIONS[0]); + + if (index < optionCount) { + ch.settings.module_settings.position_precision = POSITION_PRECISION_OPTIONS[index].value; + } + + nodeDB->saveToDisk(SEGMENT_CHANNELS); + service->reloadConfig(SEGMENT_CHANNELS); + break; + } + + case RESET_NODEDB_ALL: + InkHUD::getInstance()->notifyApplyingChanges(); + nodeDB->resetNodes(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + break; + + case RESET_NODEDB_KEEP_FAVORITES: + InkHUD::getInstance()->notifyApplyingChanges(); + nodeDB->resetNodes(1); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + break; + default: LOG_WARN("Action not implemented"); } @@ -275,9 +878,11 @@ void InkHUD::MenuApplet::showPage(MenuPage page) { items.clear(); items.shrink_to_fit(); + nodeConfigLabels.clear(); switch (page) { case ROOT: + previousPage = MenuPage::EXIT; // Optional: next applet if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1) items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown @@ -285,19 +890,24 @@ void InkHUD::MenuApplet::showPage(MenuPage page) items.push_back(MenuItem("Send", MenuPage::SEND)); items.push_back(MenuItem("Options", MenuPage::OPTIONS)); // items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO + items.push_back(MenuItem("Node Config", MenuPage::NODE_CONFIG)); items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; case SEND: populateSendPage(); + previousPage = MenuPage::ROOT; break; case CANNEDMESSAGE_RECIPIENT: populateRecipientPage(); + previousPage = MenuPage::SEND; break; case OPTIONS: + previousPage = MenuPage::ROOT; + items.push_back(MenuItem("Back", previousPage)); // Optional: backlight if (settings->optionalMenuItems.backlight) items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label @@ -305,49 +915,451 @@ void InkHUD::MenuApplet::showPage(MenuPage page) MenuPage::EXIT // Exit once complete )); - // Optional: GPS - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) - items.push_back(MenuItem("Enable GPS", MenuAction::TOGGLE_GPS, MenuPage::EXIT)); - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) - items.push_back(MenuItem("Disable GPS", MenuAction::TOGGLE_GPS, MenuPage::EXIT)); - - // Optional: Enable Bluetooth, in case of lost wifi connection - if (!config.bluetooth.enabled || config.network.wifi_enabled) - items.push_back(MenuItem("Enable Bluetooth", MenuAction::ENABLE_BLUETOOTH, MenuPage::EXIT)); - + // Options Toggles items.push_back(MenuItem("Applets", MenuPage::APPLETS)); items.push_back(MenuItem("Auto-show", MenuPage::AUTOSHOW)); items.push_back(MenuItem("Recents Duration", MenuPage::RECENTS)); if (settings->userTiles.maxCount > 1) items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS)); items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS)); + if (settings->joystick.enabled) + items.push_back(MenuItem("Align Joystick", MenuAction::ALIGN_JOYSTICK, MenuPage::EXIT)); items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS, &settings->optionalFeatures.notifications)); items.push_back(MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS, &settings->optionalFeatures.batteryIcon)); - invertedColors = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); items.push_back(MenuItem("Invert Color", MenuAction::TOGGLE_INVERT_COLOR, MenuPage::OPTIONS, &invertedColors)); - - items.push_back( - MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::OPTIONS, &config.display.use_12h_clock)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; case APPLETS: - populateAppletPage(); + previousPage = MenuPage::OPTIONS; + populateAppletPage(); // must be first + items.insert(items.begin(), MenuItem("Back", previousPage)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; case AUTOSHOW: - populateAutoshowPage(); + previousPage = MenuPage::OPTIONS; + populateAutoshowPage(); // must be first + items.insert(items.begin(), MenuItem("Back", previousPage)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; case RECENTS: - populateRecentsPage(); + previousPage = MenuPage::OPTIONS; + populateRecentsPage(); // builds only the options + items.insert(items.begin(), MenuItem("Back", previousPage)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; + case NODE_CONFIG: + previousPage = MenuPage::ROOT; + items.push_back(MenuItem("Back", previousPage)); + // Radio Config Section + items.push_back(MenuItem::Header("Radio Config")); + items.push_back(MenuItem("LoRa", MenuPage::NODE_CONFIG_LORA)); + items.push_back(MenuItem("Channel", MenuPage::NODE_CONFIG_CHANNELS)); + // Device Config Section + items.push_back(MenuItem::Header("Device Config")); + items.push_back(MenuItem("Device", MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("Position", MenuPage::NODE_CONFIG_POSITION)); + items.push_back(MenuItem("Power", MenuPage::NODE_CONFIG_POWER)); +#if defined(ARCH_ESP32) + items.push_back(MenuItem("Network", MenuPage::NODE_CONFIG_NETWORK)); +#endif + items.push_back(MenuItem("Display", MenuPage::NODE_CONFIG_DISPLAY)); + items.push_back(MenuItem("Bluetooth", MenuPage::NODE_CONFIG_BLUETOOTH)); + + // Administration Section + items.push_back(MenuItem::Header("Administration")); + items.push_back(MenuItem("Reset NodeDB", MenuPage::NODE_CONFIG_ADMIN_RESET)); + + // Exit + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + + case NODE_CONFIG_DEVICE: { + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); + + const char *role = DisplayFormatters::getDeviceRole(config.device.role); + nodeConfigLabels.emplace_back("Role: " + std::string(role)); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_DEVICE_ROLE)); + + const char *tzLabel = getTimezoneLabelFromValue(config.device.tzdef); + nodeConfigLabels.emplace_back("Timezone: " + std::string(tzLabel)); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::TIMEZONE)); + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_POSITION: { + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); +#if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS + const auto mode = config.position.gps_mode; + if (mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + items.push_back(MenuItem("GPS None", MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_POSITION)); + } else { + gpsEnabled = (mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED); + items.push_back(MenuItem("GPS", MenuAction::TOGGLE_GPS, MenuPage::NODE_CONFIG_POSITION, &gpsEnabled)); + } +#endif + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_POWER: { + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); +#if defined(ARCH_ESP32) + items.push_back(MenuItem("Powersave", MenuAction::TOGGLE_POWER_SAVE, MenuPage::EXIT, &config.power.is_power_saving)); +#endif + // ADC Multiplier + float effectiveMult = 0.0f; + + // User override always shows if it exists + if (config.power.adc_multiplier_override > 0.0f) { + effectiveMult = config.power.adc_multiplier_override; + } +#ifdef ADC_MULTIPLIER + else { + // Fallback to variant defined + effectiveMult = ADC_MULTIPLIER; + } +#endif + + // Only show if we actually have a value + if (effectiveMult > 0.0f) { + char buf[32]; + snprintf(buf, sizeof(buf), "ADC Mult: %.3f", effectiveMult); + nodeConfigLabels.emplace_back(buf); + + items.push_back( + MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_POWER_ADC_CAL)); + } + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_POWER_ADC_CAL: { + previousPage = MenuPage::NODE_CONFIG_POWER; + items.push_back(MenuItem("Back", previousPage)); + + // Instruction text (header-style, non-selectable) + items.push_back(MenuItem::Header("Run on full charge Only")); + + // Action + items.push_back(MenuItem("Calibrate ADC", MenuAction::CALIBRATE_ADC, MenuPage::NODE_CONFIG_POWER)); + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_NETWORK: { + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); + + const char *wifiLabel = config.network.wifi_enabled ? "WiFi: On" : "WiFi: Off"; + + items.push_back(MenuItem(wifiLabel, MenuAction::TOGGLE_WIFI, MenuPage::EXIT)); + +#if HAS_WIFI && defined(ARCH_ESP32) + if (config.network.wifi_enabled) { + + // Status + if (WiFi.status() == WL_CONNECTED) { + nodeConfigLabels.emplace_back("Status: Connected"); + } else { + nodeConfigLabels.emplace_back("Status: Not Connected"); + } + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_NETWORK)); + + // Signal + if (WiFi.status() == WL_CONNECTED) { + int rssi = WiFi.RSSI(); + int quality = constrain(2 * (rssi + 100), 0, 100); + + char sigBuf[32]; + snprintf(sigBuf, sizeof(sigBuf), "Signal: %d%%", quality); + nodeConfigLabels.emplace_back(sigBuf); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_NETWORK)); + + char ipBuf[64]; + snprintf(ipBuf, sizeof(ipBuf), "IP: %s", WiFi.localIP().toString().c_str()); + nodeConfigLabels.emplace_back(ipBuf); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_NETWORK)); + } + + // SSID + if (config.network.wifi_ssid && strlen(config.network.wifi_ssid) > 0) { + std::string ssidLabel = "SSID: "; + ssidLabel += config.network.wifi_ssid; + nodeConfigLabels.emplace_back(ssidLabel); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_NETWORK)); + } + + // Hostname + const char *host = WiFi.getHostname(); + if (host && strlen(host) > 0) { + std::string hostLabel = "Host: "; + hostLabel += host; + nodeConfigLabels.emplace_back(hostLabel); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_NETWORK)); + } + } +#endif + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_DISPLAY: { + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); + + items.push_back(MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::NODE_CONFIG_DISPLAY, + &config.display.use_12h_clock)); + + const char *unitsLabel = + (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "Units: Imperial" : "Units: Metric"; + + items.push_back(MenuItem(unitsLabel, MenuAction::TOGGLE_DISPLAY_UNITS, MenuPage::NODE_CONFIG_DISPLAY)); + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_BLUETOOTH: { + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); + + const char *btLabel = config.bluetooth.enabled ? "Bluetooth: On" : "Bluetooth: Off"; + items.push_back(MenuItem(btLabel, MenuAction::TOGGLE_BLUETOOTH, MenuPage::EXIT)); + + const char *pairLabel = config.bluetooth.fixed_pin ? "Pair Mode: Fixed" : "Pair Mode: Random"; + items.push_back(MenuItem(pairLabel, MenuAction::TOGGLE_BLUETOOTH_PAIR_MODE, MenuPage::NODE_CONFIG_BLUETOOTH)); + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_LORA: { + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); + + const char *region = myRegion ? myRegion->name : "Unset"; + nodeConfigLabels.emplace_back("Region: " + std::string(region)); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::REGION)); + + const char *preset = + DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); + nodeConfigLabels.emplace_back("Preset: " + std::string(preset)); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_PRESET)); + + char freqBuf[32]; + float freq = RadioLibInterface::instance->getFreq(); + snprintf(freqBuf, sizeof(freqBuf), "Freq: %.3f MHz", freq); + nodeConfigLabels.emplace_back(freqBuf); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_LORA)); + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_CHANNELS: { + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); + + for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { + meshtastic_Channel &ch = channels.getByIndex(i); + + if (!ch.has_settings) + continue; + + if (ch.role == meshtastic_Channel_Role_DISABLED) + continue; + + std::string label = "#"; + + if (ch.role == meshtastic_Channel_Role_PRIMARY) { + label += "Primary"; + } else if (strlen(ch.settings.name) > 0) { + label += parse(ch.settings.name); + } else { + label += "Channel" + to_string(i + 1); + } + + nodeConfigLabels.push_back(label); + items.push_back( + MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); + } + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_CHANNEL_DETAIL: { + previousPage = MenuPage::NODE_CONFIG_CHANNELS; + items.push_back(MenuItem("Back", previousPage)); + + meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex); + + // Name (read-only) + const char *name = strlen(ch.settings.name) > 0 ? ch.settings.name : "Unnamed"; + nodeConfigLabels.emplace_back("Ch: " + parse(name)); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); + + // Uplink + items.push_back(MenuItem("Uplink", MenuAction::TOGGLE_CHANNEL_UPLINK, MenuPage::NODE_CONFIG_CHANNEL_DETAIL, + &ch.settings.uplink_enabled)); + + items.push_back(MenuItem("Downlink", MenuAction::TOGGLE_CHANNEL_DOWNLINK, MenuPage::NODE_CONFIG_CHANNEL_DETAIL, + &ch.settings.downlink_enabled)); + + // Position + channelPositionEnabled = ch.settings.has_module_settings && ch.settings.module_settings.position_precision > 0; + + items.push_back(MenuItem("Position", MenuAction::TOGGLE_CHANNEL_POSITION, MenuPage::NODE_CONFIG_CHANNEL_DETAIL, + &channelPositionEnabled)); + + // Precision + if (channelPositionEnabled) { + + std::string precisionLabel = "Unknown"; + + for (const auto &opt : POSITION_PRECISION_OPTIONS) { + if (opt.value == ch.settings.module_settings.position_precision) { + precisionLabel = (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + ? opt.imperial + : opt.metric; + break; + } + } + nodeConfigLabels.emplace_back("Precision: " + precisionLabel); + items.push_back( + MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_CHANNEL_PRECISION)); + } + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_CHANNEL_PRECISION: { + previousPage = MenuPage::NODE_CONFIG_CHANNEL_DETAIL; + items.push_back(MenuItem("Back", previousPage)); + meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex); + if (!ch.settings.has_module_settings || ch.settings.module_settings.position_precision == 0) { + items.push_back(MenuItem("Position is Off", MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); + break; + } + constexpr uint8_t optionCount = sizeof(POSITION_PRECISION_OPTIONS) / sizeof(POSITION_PRECISION_OPTIONS[0]); + for (uint8_t i = 0; i < optionCount; i++) { + const auto &opt = POSITION_PRECISION_OPTIONS[i]; + const char *label = + (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? opt.imperial : opt.metric; + nodeConfigLabels.emplace_back(label); + + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::SET_CHANNEL_PRECISION, + MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); + } + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_DEVICE_ROLE: { + previousPage = MenuPage::NODE_CONFIG_DEVICE; + items.push_back(MenuItem("Back", previousPage)); + items.push_back(MenuItem("Client", MenuAction::SET_ROLE_CLIENT, MenuPage::EXIT)); + items.push_back(MenuItem("Client Mute", MenuAction::SET_ROLE_CLIENT_MUTE, MenuPage::EXIT)); + items.push_back(MenuItem("Router", MenuAction::SET_ROLE_ROUTER, MenuPage::EXIT)); + items.push_back(MenuItem("Repeater", MenuAction::SET_ROLE_REPEATER, MenuPage::EXIT)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case TIMEZONE: + previousPage = MenuPage::NODE_CONFIG_DEVICE; + items.push_back(MenuItem("Back", previousPage)); + items.push_back(MenuItem("US/Hawaii", SET_TZ_US_HAWAII, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("US/Alaska", SET_TZ_US_ALASKA, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("US/Pacific", SET_TZ_US_PACIFIC, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("US/Arizona", SET_TZ_US_ARIZONA, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("US/Mountain", SET_TZ_US_MOUNTAIN, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("US/Central", SET_TZ_US_CENTRAL, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("US/Eastern", SET_TZ_US_EASTERN, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("BR/Brasilia", SET_TZ_BR_BRAZILIA, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("UTC", SET_TZ_UTC, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("EU/Western", SET_TZ_EU_WESTERN, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("EU/Central", SET_TZ_EU_CENTRAL, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("EU/Eastern", SET_TZ_EU_EASTERN, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("Asia/Kolkata", SET_TZ_ASIA_KOLKATA, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("Asia/Hong Kong", SET_TZ_ASIA_HONG_KONG, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("AU/AWST", SET_TZ_AU_AWST, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("AU/ACST", SET_TZ_AU_ACST, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("AU/AEST", SET_TZ_AU_AEST, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + + case REGION: + previousPage = MenuPage::NODE_CONFIG_LORA; + items.push_back(MenuItem("Back", previousPage)); + items.push_back(MenuItem("US", MenuAction::SET_REGION_US, MenuPage::EXIT)); + items.push_back(MenuItem("EU 868", MenuAction::SET_REGION_EU_868, MenuPage::EXIT)); + items.push_back(MenuItem("EU 433", MenuAction::SET_REGION_EU_433, MenuPage::EXIT)); + items.push_back(MenuItem("CN", MenuAction::SET_REGION_CN, MenuPage::EXIT)); + items.push_back(MenuItem("JP", MenuAction::SET_REGION_JP, MenuPage::EXIT)); + items.push_back(MenuItem("ANZ", MenuAction::SET_REGION_ANZ, MenuPage::EXIT)); + items.push_back(MenuItem("KR", MenuAction::SET_REGION_KR, MenuPage::EXIT)); + items.push_back(MenuItem("TW", MenuAction::SET_REGION_TW, MenuPage::EXIT)); + items.push_back(MenuItem("RU", MenuAction::SET_REGION_RU, MenuPage::EXIT)); + items.push_back(MenuItem("IN", MenuAction::SET_REGION_IN, MenuPage::EXIT)); + items.push_back(MenuItem("NZ 865", MenuAction::SET_REGION_NZ_865, MenuPage::EXIT)); + items.push_back(MenuItem("TH", MenuAction::SET_REGION_TH, MenuPage::EXIT)); + items.push_back(MenuItem("LoRa 2.4", MenuAction::SET_REGION_LORA_24, MenuPage::EXIT)); + items.push_back(MenuItem("UA 433", MenuAction::SET_REGION_UA_433, MenuPage::EXIT)); + items.push_back(MenuItem("UA 868", MenuAction::SET_REGION_UA_868, MenuPage::EXIT)); + items.push_back(MenuItem("MY 433", MenuAction::SET_REGION_MY_433, MenuPage::EXIT)); + items.push_back(MenuItem("MY 919", MenuAction::SET_REGION_MY_919, MenuPage::EXIT)); + items.push_back(MenuItem("SG 923", MenuAction::SET_REGION_SG_923, MenuPage::EXIT)); + items.push_back(MenuItem("PH 433", MenuAction::SET_REGION_PH_433, MenuPage::EXIT)); + items.push_back(MenuItem("PH 868", MenuAction::SET_REGION_PH_868, MenuPage::EXIT)); + items.push_back(MenuItem("PH 915", MenuAction::SET_REGION_PH_915, MenuPage::EXIT)); + items.push_back(MenuItem("ANZ 433", MenuAction::SET_REGION_ANZ_433, MenuPage::EXIT)); + items.push_back(MenuItem("KZ 433", MenuAction::SET_REGION_KZ_433, MenuPage::EXIT)); + items.push_back(MenuItem("KZ 863", MenuAction::SET_REGION_KZ_863, MenuPage::EXIT)); + items.push_back(MenuItem("NP 865", MenuAction::SET_REGION_NP_865, MenuPage::EXIT)); + items.push_back(MenuItem("BR 902", MenuAction::SET_REGION_BR_902, MenuPage::EXIT)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + + case NODE_CONFIG_PRESET: { + previousPage = MenuPage::NODE_CONFIG_LORA; + items.push_back(MenuItem("Back", previousPage)); + items.push_back(MenuItem("Long Moderate", MenuAction::SET_PRESET_LONG_MODERATE, MenuPage::EXIT)); + items.push_back(MenuItem("Long Fast", MenuAction::SET_PRESET_LONG_FAST, MenuPage::EXIT)); + items.push_back(MenuItem("Medium Slow", MenuAction::SET_PRESET_MEDIUM_SLOW, MenuPage::EXIT)); + items.push_back(MenuItem("Medium Fast", MenuAction::SET_PRESET_MEDIUM_FAST, MenuPage::EXIT)); + items.push_back(MenuItem("Short Slow", MenuAction::SET_PRESET_SHORT_SLOW, MenuPage::EXIT)); + items.push_back(MenuItem("Short Fast", MenuAction::SET_PRESET_SHORT_FAST, MenuPage::EXIT)); + items.push_back(MenuItem("Short Turbo", MenuAction::SET_PRESET_SHORT_TURBO, MenuPage::EXIT)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + // Administration Section + case NODE_CONFIG_ADMIN_RESET: + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); + items.push_back(MenuItem("Reset All", MenuAction::RESET_NODEDB_ALL, MenuPage::EXIT)); + items.push_back(MenuItem("Keep Favorites Only", MenuAction::RESET_NODEDB_KEEP_FAVORITES, MenuPage::EXIT)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + + // Exit case EXIT: sendToBackground(); // Menu applet dismissed, allow normal behavior to resume break; @@ -366,18 +1378,34 @@ void InkHUD::MenuApplet::showPage(MenuPage page) cursorShown = false; } + // Ensure cursor never rests on a header + if (cursorShown) { + while (cursor < items.size() && items.at(cursor).isHeader) { + cursor++; + } + if (cursor >= items.size()) + cursor = 0; + } + // Remember which page we are on now currentPage = page; } -void InkHUD::MenuApplet::onRender() +void InkHUD::MenuApplet::onRender(bool full) { + // Free text mode draws a text input field and skips the normal rendering + if (freeTextMode) { + drawInputField(0, fontSmall.lineHeight(), X(1.0), Y(1.0) - fontSmall.lineHeight() - 1, cm.freeTextItem.rawText); + return; + } + if (items.size() == 0) LOG_ERROR("Empty Menu"); // Dimensions for the slots where we will draw menuItems const float padding = 0.05; - const uint16_t itemH = fontSmall.lineHeight() * 2; + const uint16_t itemH = fontSmall.lineHeight() * 1.6; + const int16_t selectInsetY = 2; const int16_t itemW = width() - X(padding) - X(padding); const int16_t itemL = X(padding); const int16_t itemR = X(1 - padding); @@ -428,18 +1456,31 @@ void InkHUD::MenuApplet::onRender() // -- Loop: draw each (visible) menu item -- for (uint8_t i = firstItem; i <= lastItem; i++) { - // Grab the menuItem - MenuItem item = items.at(i); - // Center-line for the text + // Grab the menu item + MenuItem &item = items.at(i); + + // Vertical center of this slot int16_t center = itemT + (itemH / 2); - // Box, if currently selected - if (cursorShown && i == cursor) - drawRect(itemL, itemT, itemW, itemH, BLACK); + // Header (non-selectable section label) + if (item.isHeader) { + setFont(fontSmall); - // Item's text - printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE); + // Header text (flush left) + printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE); + + // Subtle underline + int16_t underlineY = itemT + itemH - 2; + drawLine(itemL + X(padding), underlineY, itemR - X(padding), underlineY, BLACK); + } else { + // Box, if currently selected + if (cursorShown && i == cursor) + drawRect(itemL, itemT + selectInsetY, itemW, itemH - (selectInsetY * 2), BLACK); + + // Indented normal item text + printAt(itemL + X(padding * 2), center, item.label, LEFT, MIDDLE); + } // Checkbox, if relevant if (item.checkState) { @@ -476,32 +1517,161 @@ void InkHUD::MenuApplet::onRender() void InkHUD::MenuApplet::onButtonShortPress() { - // Push the auto-close timer back - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + if (!freeTextMode) { + // Push the auto-close timer back + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - // Move menu cursor to next entry, then update - if (cursorShown) - cursor = (cursor + 1) % items.size(); - else - cursorShown = true; - requestUpdate(Drivers::EInk::UpdateTypes::FAST); + if (!settings->joystick.enabled) { + if (!cursorShown) { + cursorShown = true; + cursor = 0; + } else { + do { + cursor = (cursor + 1) % items.size(); + } while (items.at(cursor).isHeader); + } + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } else { + if (cursorShown) + execute(items.at(cursor)); + else + showPage(MenuPage::EXIT); + if (!wantsToRender()) + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } + } } void InkHUD::MenuApplet::onButtonLongPress() { - // Push the auto-close timer back - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + if (!freeTextMode) { + // Push the auto-close timer back + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - if (cursorShown) - execute(items.at(cursor)); - else - showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close + if (cursorShown) + execute(items.at(cursor)); + else + showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close + + // If we didn't already request a specialized update, when handling a menu action, + // then perform the usual fast update. + // FAST keeps things responsive: important because we're dealing with user input + if (!wantsToRender()) + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } +} + +void InkHUD::MenuApplet::onExitShort() +{ + // Exit the menu + showPage(MenuPage::EXIT); + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onNavUp() +{ + if (!freeTextMode) { + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + if (!cursorShown) { + cursorShown = true; + cursor = 0; + } else { + do { + if (cursor == 0) + cursor = items.size() - 1; + else + cursor--; + } while (items.at(cursor).isHeader); + } - // If we didn't already request a specialized update, when handling a menu action, - // then perform the usual fast update. - // FAST keeps things responsive: important because we're dealing with user input - if (!wantsToRender()) requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } +} + +void InkHUD::MenuApplet::onNavDown() +{ + if (!freeTextMode) { + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + if (!cursorShown) { + cursorShown = true; + cursor = 0; + } else { + do { + cursor = (cursor + 1) % items.size(); + } while (items.at(cursor).isHeader); + } + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } +} + +void InkHUD::MenuApplet::onNavLeft() +{ + if (!freeTextMode) { + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + // Go to the previous menu page + showPage(previousPage); + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } +} + +void InkHUD::MenuApplet::onNavRight() +{ + if (!freeTextMode) { + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + if (cursorShown) + execute(items.at(cursor)); + if (!wantsToRender()) + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } +} + +void InkHUD::MenuApplet::onFreeText(char c) +{ + if (cm.freeTextItem.rawText.length() >= menuTextLimit && c != '\b') + return; + if (c == '\b') { + if (!cm.freeTextItem.rawText.empty()) + cm.freeTextItem.rawText.pop_back(); + } else { + cm.freeTextItem.rawText += c; + } + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onFreeTextDone() +{ + // Restart the auto-close timeout + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + OSThread::enabled = true; + + handleFreeText = false; + freeTextMode = false; + + if (!cm.freeTextItem.rawText.empty()) { + cm.selectedMessageItem = &cm.freeTextItem; + showPage(MenuPage::CANNEDMESSAGE_RECIPIENT); + } + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onFreeTextCancel() +{ + // Restart the auto-close timeout + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + OSThread::enabled = true; + + handleFreeText = false; + freeTextMode = false; + + // Clear the free text message + cm.freeTextItem.rawText.erase(); + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); } // Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu @@ -544,7 +1714,8 @@ void InkHUD::MenuApplet::populateRecentsPage() // (Defined at top of this file) for (uint8_t i = 0; i < optionCount; i++) { std::string label = to_string(RECENTS_OPTIONS_MINUTES[i]) + " mins"; - items.push_back(MenuItem(label.c_str(), MenuAction::SET_RECENTS, MenuPage::EXIT)); + recentsSelected[i] = (settings->recentlyActiveSeconds == RECENTS_OPTIONS_MINUTES[i] * 60); + items.push_back(MenuItem(label.c_str(), MenuAction::SET_RECENTS, MenuPage::OPTIONS, &recentsSelected[i])); } } @@ -555,6 +1726,10 @@ void InkHUD::MenuApplet::populateSendPage() // Position / NodeInfo packet items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); + // If joystick is available, include the Free Text option + if (settings->joystick.enabled) + items.push_back(MenuItem("Free Text", MenuAction::FREE_TEXT, MenuPage::SEND)); + // One menu item for each canned message uint8_t count = cm.store->size(); for (uint8_t i = 0; i < count; i++) { @@ -654,6 +1829,48 @@ void InkHUD::MenuApplet::populateRecipientPage() items.push_back(MenuItem("Exit", MenuPage::EXIT)); } +void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, std::string text) +{ + setFont(fontSmall); + uint16_t wrapMaxH = 0; + + // Draw the text, input box, and cursor + // Adjusting the box for screen height + while (wrapMaxH < height - fontSmall.lineHeight()) { + wrapMaxH += fontSmall.lineHeight(); + } + + // If the text is so long that it goes outside of the input box, the text is actually rendered off screen. + uint32_t textHeight = getWrappedTextHeight(0, width - 5, text); + if (!text.empty()) { + uint16_t textPadding = X(1.0) > Y(1.0) ? wrapMaxH - textHeight : wrapMaxH - textHeight + 1; + if (textHeight > wrapMaxH) + printWrapped(2, textPadding, width - 5, text); + else + printWrapped(2, top + 2, width - 5, text); + } + + uint16_t textCursorX = text.empty() ? 1 : getCursorX(); + uint16_t textCursorY = text.empty() ? fontSmall.lineHeight() + 2 : getCursorY() - fontSmall.lineHeight() + 3; + + if (textCursorX + 1 > width - 5) { + textCursorX = getCursorX() - width + 5; + textCursorY += fontSmall.lineHeight(); + } + + fillRect(textCursorX + 1, textCursorY, 1, fontSmall.lineHeight(), BLACK); + + // A white rectangle clears the top part of the screen for any text that's printed beyond the input box + fillRect(0, 0, X(1.0), top, WHITE); + + // Draw character limit + std::string ftlen = std::to_string(text.length()) + "/" + to_string(menuTextLimit); + uint16_t textLen = getTextWidth(ftlen); + printAt(X(1.0) - textLen - 2, 0, ftlen); + + // Draw the border + drawRect(0, top, width, wrapMaxH + 5, BLACK); +} // 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. @@ -795,5 +2012,4 @@ void InkHUD::MenuApplet::freeCannedMessageResources() cm.messageItems.clear(); cm.recipientItems.clear(); } - -#endif \ No newline at end of file +#endif // MESHTASTIC_INCLUDE_INKHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h index 8f9280e6f..7b092153b 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h @@ -27,9 +27,18 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void onBackground() override; void onButtonShortPress() override; void onButtonLongPress() override; - void onRender() override; + void onExitShort() override; + void onNavUp() override; + void onNavDown() override; + void onNavLeft() override; + void onNavRight() override; + void onFreeText(char c) override; + void onFreeTextDone() override; + void onFreeTextCancel() override; + void onRender(bool full) override; void show(Tile *t); // Open the menu, onto a user tile + void setStartPage(MenuPage page); protected: Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton @@ -45,19 +54,32 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds + void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, + std::string text); // Draw input field for free text 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 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 startPageOverride = MenuPage::ROOT; MenuPage currentPage = MenuPage::ROOT; + MenuPage previousPage = MenuPage::EXIT; uint8_t cursor = 0; // Which menu item is currently highlighted bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection) - + bool freeTextMode = false; uint16_t systemInfoPanelHeight = 0; // Need to know before we render + uint16_t menuTextLimit = 200; - std::vector items; // MenuItems for the current page. Filled by ShowPage + std::vector items; // MenuItems for the current page. Filled by ShowPage + std::vector nodeConfigLabels; // Persistent labels for Node Config pages + uint8_t selectedChannelIndex = 0; // Currently selected LoRa channel (Node Config → Radio → Channel) + bool channelPositionEnabled = false; + bool gpsEnabled = false; + + // Recents menu checkbox state (derived from settings.recentlyActiveSeconds) + static constexpr uint8_t RECENTS_COUNT = 6; + bool recentsSelected[RECENTS_COUNT] = {}; // Data for selecting and sending canned messages via the menu // Placed into a sub-class for organization only @@ -88,6 +110,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread // Cleared onBackground (when MenuApplet closes) std::vector messageItems; std::vector recipientItems; + + MessageItem freeTextItem; } cm; Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu @@ -97,4 +121,4 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h index c74fe3d8a..51c9161a7 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h @@ -30,6 +30,7 @@ class MenuItem MenuAction action = NO_ACTION; MenuPage nextPage = EXIT; bool *checkState = nullptr; + bool isHeader = false; // Non-selectable section label // Various constructors, depending on the intended function of the item @@ -40,6 +41,12 @@ class MenuItem : label(label), action(action), nextPage(nextPage), checkState(checkState) { } + static MenuItem Header(const char *label) + { + MenuItem item(label, NO_ACTION, EXIT); + item.isHeader = true; + return item; + } }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h index 389e411c3..138c389be 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h @@ -20,10 +20,27 @@ enum MenuPage : uint8_t { SEND, CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message OPTIONS, + NODE_CONFIG, + NODE_CONFIG_LORA, + NODE_CONFIG_CHANNELS, // List of channels + NODE_CONFIG_CHANNEL_DETAIL, // Per-channel options + NODE_CONFIG_CHANNEL_PRECISION, + NODE_CONFIG_PRESET, + NODE_CONFIG_DEVICE, + NODE_CONFIG_DEVICE_ROLE, + NODE_CONFIG_POWER, + NODE_CONFIG_POWER_ADC_CAL, + NODE_CONFIG_NETWORK, + NODE_CONFIG_DISPLAY, + NODE_CONFIG_BLUETOOTH, + NODE_CONFIG_POSITION, + NODE_CONFIG_ADMIN_RESET, + TIMEZONE, APPLETS, AUTOSHOW, RECENTS, // Select length of "recentlyActiveSeconds" - EXIT, // Dismiss the menu applet + REGION, + EXIT, // Dismiss the menu applet }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp index ae0836d19..19cef4fbd 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp @@ -65,7 +65,7 @@ int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket return 0; } -void InkHUD::NotificationApplet::onRender() +void InkHUD::NotificationApplet::onRender(bool full) { // Clear the region beneath the tile // Most applets are drawing onto an empty frame buffer and don't need to do this @@ -139,18 +139,47 @@ void InkHUD::NotificationApplet::onForeground() void InkHUD::NotificationApplet::onBackground() { handleInput = false; + inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); } void InkHUD::NotificationApplet::onButtonShortPress() { dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } void InkHUD::NotificationApplet::onButtonLongPress() { dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::NotificationApplet::onExitShort() +{ + dismiss(); +} + +void InkHUD::NotificationApplet::onExitLong() +{ + dismiss(); +} + +void InkHUD::NotificationApplet::onNavUp() +{ + dismiss(); +} + +void InkHUD::NotificationApplet::onNavDown() +{ + dismiss(); +} + +void InkHUD::NotificationApplet::onNavLeft() +{ + dismiss(); +} + +void InkHUD::NotificationApplet::onNavRight() +{ + dismiss(); } // Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h index 66df784b4..d398a36f3 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h @@ -26,11 +26,17 @@ class NotificationApplet : public SystemApplet public: NotificationApplet(); - void onRender() override; + void onRender(bool full) override; void onForeground() override; void onBackground() override; void onButtonShortPress() override; void onButtonLongPress() override; + void onExitShort() override; + void onExitLong() override; + void onNavUp() override; + void onNavDown() override; + void onNavLeft() override; + void onNavRight() override; int onReceiveTextMessage(const meshtastic_MeshPacket *p); diff --git a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp index 09931f109..a09ff55d5 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp @@ -9,7 +9,7 @@ InkHUD::PairingApplet::PairingApplet() bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); } -void InkHUD::PairingApplet::onRender() +void InkHUD::PairingApplet::onRender(bool full) { // Header setFont(fontMedium); @@ -45,7 +45,7 @@ void InkHUD::PairingApplet::onBackground() // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); } int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status) diff --git a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h index b89783a25..4c2e95321 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h @@ -22,7 +22,7 @@ class PairingApplet : public SystemApplet public: PairingApplet(); - void onRender() override; + void onRender(bool full) override; void onForeground() override; void onBackground() override; diff --git a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp index 99cdeb0ac..228c8b2ca 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp @@ -4,7 +4,7 @@ using namespace NicheGraphics; -void InkHUD::PlaceholderApplet::onRender() +void InkHUD::PlaceholderApplet::onRender(bool full) { // This placeholder applet fills its area with sparse diagonal lines hatchRegion(0, 0, width(), height(), 8, BLACK); diff --git a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h index 78ba5cd89..fa40913e0 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h @@ -17,7 +17,7 @@ namespace NicheGraphics::InkHUD class PlaceholderApplet : public SystemApplet { public: - void onRender() override; + void onRender(bool full) override; // Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet. // The window manager decides when and where it should be rendered diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp index ade44ab65..6cac2644b 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp @@ -10,39 +10,42 @@ using namespace NicheGraphics; InkHUD::TipsApplet::TipsApplet() { - // Decide which tips (if any) should be shown to user after the boot screen + bool needsRegion = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET); + + bool showTutorialTips = (settings->tips.firstBoot || needsRegion); // Welcome screen - if (settings->tips.firstBoot) + if (showTutorialTips) tipQueue.push_back(Tip::WELCOME); - // Antenna, region, timezone - // Shown at boot if region not yet set - if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) + // Finish setup + if (needsRegion) tipQueue.push_back(Tip::FINISH_SETUP); + // Using the UI + if (showTutorialTips) { + tipQueue.push_back(Tip::CUSTOMIZATION); + tipQueue.push_back(Tip::BUTTONS); + } + // Shutdown info // Shown until user performs one valid shutdown if (!settings->tips.safeShutdownSeen) tipQueue.push_back(Tip::SAFE_SHUTDOWN); - // Using the UI - if (settings->tips.firstBoot) { - tipQueue.push_back(Tip::CUSTOMIZATION); - tipQueue.push_back(Tip::BUTTONS); - } - // Catch an incorrect attempt at rotating display if (config.display.flip_screen) tipQueue.push_back(Tip::ROTATION); - // Applet is foreground immediately at boot, but is obscured by LogoApplet, which is also foreground - // LogoApplet can be considered to have a higher Z-index, because it is placed before TipsApplet in the systemApplets vector + // Region picker + if (needsRegion) + tipQueue.push_back(Tip::PICK_REGION); + if (!tipQueue.empty()) bringToForeground(); } -void InkHUD::TipsApplet::onRender() +void InkHUD::TipsApplet::onRender(bool full) { switch (tipQueue.front()) { case Tip::WELCOME: @@ -51,84 +54,135 @@ void InkHUD::TipsApplet::onRender() case Tip::FINISH_SETUP: { setFont(fontMedium); - printAt(0, 0, "Tip: Finish Setup"); + const char *title = "Tip: Finish Setup"; + uint16_t h = getWrappedTextHeight(0, width(), title); + printWrapped(0, 0, width(), title); setFont(fontSmall); - int16_t cursorY = fontMedium.lineHeight() * 1.5; - printAt(0, cursorY, "- connect antenna"); + int16_t cursorY = h + fontSmall.lineHeight(); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- connect a client app"); + auto drawBullet = [&](const char *text) { + uint16_t bh = getWrappedTextHeight(0, width(), text); + printWrapped(0, cursorY, width(), text); + cursorY += bh + (fontSmall.lineHeight() / 3); + }; - // Only if region not set - if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- set region"); - } + drawBullet("- connect antenna"); + drawBullet("- connect a client app"); - // Only if tz not set - if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) { - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- set timezone"); - } + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) + drawBullet("- set region"); - cursorY += fontSmall.lineHeight() * 1.5; - printAt(0, cursorY, "More info at meshtastic.org"); + if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) + drawBullet("- set timezone"); + + cursorY += fontSmall.lineHeight() / 2; + drawBullet("More info at meshtastic.org"); - setFont(fontSmall); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); } break; + case Tip::PICK_REGION: { + setFont(fontMedium); + printAt(0, 0, "Set Region"); + + setFont(fontSmall); + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), "Please select your LoRa region to complete setup."); + + printAt(0, Y(1.0), "Press button to choose", LEFT, BOTTOM); + } break; + case Tip::SAFE_SHUTDOWN: { setFont(fontMedium); - printAt(0, 0, "Tip: Shutdown"); + + const char *title = "Tip: Shutdown"; + uint16_t h = getWrappedTextHeight(0, width(), title); + printWrapped(0, 0, width(), title); setFont(fontSmall); - std::string shutdown; - shutdown += "Before removing power, please shut down from InkHUD menu, or a client app. \n"; - shutdown += "\n"; - shutdown += "This ensures data is saved."; - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), shutdown); + int16_t cursorY = h + fontSmall.lineHeight(); + + const char *body = "Before removing power, please shut down from InkHUD menu, or a client app.\n\n" + "This ensures data is saved."; + + uint16_t bodyH = getWrappedTextHeight(0, width(), body); + printWrapped(0, cursorY, width(), body); + cursorY += bodyH + (fontSmall.lineHeight() / 2); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); - } break; case Tip::CUSTOMIZATION: { setFont(fontMedium); - printAt(0, 0, "Tip: Customization"); + + const char *title = "Tip: Customization"; + uint16_t h = getWrappedTextHeight(0, width(), title); + printWrapped(0, 0, width(), title); setFont(fontSmall); - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), - "Configure & control display with the InkHUD menu. Optional features, layout, rotation, and more."); + int16_t cursorY = h + fontSmall.lineHeight(); + + const char *body = "Configure & control display with the InkHUD menu. " + "Optional features, layout, rotation, and more."; + + uint16_t bodyH = getWrappedTextHeight(0, width(), body); + printWrapped(0, cursorY, width(), body); + cursorY += bodyH + (fontSmall.lineHeight() / 2); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); } break; case Tip::BUTTONS: { setFont(fontMedium); - printAt(0, 0, "Tip: Buttons"); + + const char *title = "Tip: Buttons"; + uint16_t h = getWrappedTextHeight(0, width(), title); + printWrapped(0, 0, width(), title); setFont(fontSmall); - int16_t cursorY = fontMedium.lineHeight() * 1.5; + int16_t cursorY = h + fontSmall.lineHeight(); - printAt(0, cursorY, "User Button"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- short press: next"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- long press: select / open menu"); - cursorY += fontSmall.lineHeight() * 1.5; + auto drawBullet = [&](const char *text) { + uint16_t bh = getWrappedTextHeight(0, width(), text); + printWrapped(0, cursorY, width(), text); + cursorY += bh + (fontSmall.lineHeight() / 3); + }; + + if (!settings->joystick.enabled) { + drawBullet("User Button"); + drawBullet("- short press: next"); + drawBullet("- long press: select or open menu"); + } else { + drawBullet("Joystick"); + drawBullet("- press: open menu or select"); + drawBullet("Exit Button"); + drawBullet("- press: switch tile or close menu"); + } printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); } break; case Tip::ROTATION: { setFont(fontMedium); - printAt(0, 0, "Tip: Rotation"); + + const char *title = "Tip: Rotation"; + uint16_t h = getWrappedTextHeight(0, width(), title); + printWrapped(0, 0, width(), title); setFont(fontSmall); - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), - "To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate."); + if (!settings->joystick.enabled) { + int16_t cursorY = h + fontSmall.lineHeight(); + + const char *body = "To rotate the display, use the InkHUD menu. " + "Long-press the user button > Options > Rotate."; + + uint16_t bh = getWrappedTextHeight(0, width(), body); + printWrapped(0, cursorY, width(), body); + cursorY += bh + (fontSmall.lineHeight() / 2); + } else { + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), + "To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate."); + } printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); @@ -145,12 +199,15 @@ void InkHUD::TipsApplet::renderWelcome() { uint16_t padW = X(0.05); + // Detect portrait orientation + bool portrait = height() > width(); + // Block 1 - logo & title // ======================== // Logo size - uint16_t logoWLimit = X(0.3); - uint16_t logoHLimit = Y(0.3); + uint16_t logoWLimit = portrait ? X(0.5) : X(0.3); + uint16_t logoHLimit = portrait ? Y(0.25) : Y(0.3); uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit); uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); @@ -163,7 +220,7 @@ void InkHUD::TipsApplet::renderWelcome() // Center the block // Desired effect: equal margin from display edge for logo left and title right - int16_t block1Y = Y(0.3); + int16_t block1Y = portrait ? Y(0.2) : Y(0.3); int16_t block1CX = X(0.5) + (logoW / 2) - (titleW / 2); int16_t logoCX = block1CX - (logoW / 2) - (padW / 2); int16_t titleCX = block1CX + (titleW / 2) + (padW / 2); @@ -178,7 +235,7 @@ void InkHUD::TipsApplet::renderWelcome() std::string subtitle = "InkHUD"; if (width() >= 200) subtitle += " - A Heads-Up Display"; // Future proofing: narrower for tiny display - printAt(X(0.5), Y(0.6), subtitle, CENTER, MIDDLE); + printAt(X(0.5), portrait ? Y(0.45) : Y(0.6), subtitle, CENTER, MIDDLE); // Block 3 - press to continue // ============================ @@ -204,32 +261,48 @@ void InkHUD::TipsApplet::onBackground() // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); } // While our SystemApplet::handleInput flag is true void InkHUD::TipsApplet::onButtonShortPress() { + bool needsRegion = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET); + // If we're prompting the user to pick a region, hand off to the menu + if (!tipQueue.empty() && tipQueue.front() == Tip::PICK_REGION) { + tipQueue.pop_front(); + + // Signal InkHUD to open the menu on Region page + inkhud->forceRegionMenu = true; + + // Close tips and open menu + sendToBackground(); + inkhud->openMenu(); + return; + } + // Consume current tip tipQueue.pop_front(); // All tips done if (tipQueue.empty()) { // Record that user has now seen the "tutorial" set of tips // Don't show them on subsequent boots - if (settings->tips.firstBoot) { + if (settings->tips.firstBoot && !needsRegion) { settings->tips.firstBoot = false; inkhud->persistence->saveSettings(); } - // Close applet, and full refresh to clean the screen - // Need to force update, because our request would be ignored otherwise, as we are now background + // Close applet sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); - } - - // More tips left - else + } else { requestUpdate(); + } } -#endif \ No newline at end of file +// Functions the same as the user button in this instance +void InkHUD::TipsApplet::onExitShort() +{ + onButtonShortPress(); +} + +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h index db88585e9..2e81d678b 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h @@ -23,6 +23,7 @@ class TipsApplet : public SystemApplet enum class Tip { WELCOME, FINISH_SETUP, + PICK_REGION, SAFE_SHUTDOWN, CUSTOMIZATION, BUTTONS, @@ -32,10 +33,11 @@ class TipsApplet : public SystemApplet public: TipsApplet(); - void onRender() override; + void onRender(bool full) override; void onForeground() override; void onBackground() override; void onButtonShortPress() override; + void onExitShort() override; protected: void renderWelcome(); // Very first screen of tutorial diff --git a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp index 7c6232f3b..96c519599 100644 --- a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp @@ -34,7 +34,7 @@ int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket * return 0; } -void InkHUD::AllMessageApplet::onRender() +void InkHUD::AllMessageApplet::onRender(bool full) { // Find newest message, regardless of whether DM or broadcast MessageStore::Message *message; diff --git a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h index c74e16196..4aa97e4f1 100644 --- a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h @@ -30,7 +30,7 @@ class Applet; class AllMessageApplet : public Applet { public: - void onRender() override; + void onRender(bool full) override; void onActivate() override; void onDeactivate() override; diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp index a3b9615a5..189a56cab 100644 --- a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp @@ -37,7 +37,7 @@ int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) return 0; } -void InkHUD::DMApplet::onRender() +void InkHUD::DMApplet::onRender(bool full) { // Abort if no text message if (!latestMessage->dm.sender) { diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h index b3dc36e66..4eb0ec704 100644 --- a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h @@ -30,7 +30,7 @@ class Applet; class DMApplet : public Applet { public: - void onRender() override; + void onRender(bool full) override; void onActivate() override; void onDeactivate() override; diff --git a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp index 88bed998d..ae7679962 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp @@ -1,13 +1,14 @@ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./PositionsApplet.h" +#include "NodeDB.h" using namespace NicheGraphics; -void InkHUD::PositionsApplet::onRender() +void InkHUD::PositionsApplet::onRender(bool full) { // Draw the usual map applet first - MapApplet::onRender(); + MapApplet::onRender(full); // Draw our latest "node of interest" as a special marker // ------------------------------------------------------- @@ -49,8 +50,8 @@ ProcessMessage InkHUD::PositionsApplet::handleReceived(const meshtastic_MeshPack if (!hasPosition) return ProcessMessage::CONTINUE; - bool hasHopsAway = (mp.hop_start != 0 && mp.hop_limit <= mp.hop_start); // From NodeDB::updateFrom - uint8_t hopsAway = mp.hop_start - mp.hop_limit; + const int8_t hopsAway = getHopsAway(mp); + const bool hasHopsAway = hopsAway >= 0; // Determine if the position packet would change anything on-screen // ----------------------------------------------------------------- diff --git a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h index 28a53cb0f..d0d3e5f07 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h @@ -24,7 +24,7 @@ class PositionsApplet : public MapApplet, public SinglePortModule { public: PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {} - void onRender() override; + void onRender(bool full) override; protected: ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp index fdb5a168d..f16721357 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp @@ -22,7 +22,7 @@ InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) store = new MessageStore("ch" + to_string(channelIndex)); } -void InkHUD::ThreadedMessageApplet::onRender() +void InkHUD::ThreadedMessageApplet::onRender(bool full) { // ============= // Draw a header diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h index c986539b3..045e2a6fc 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h @@ -36,7 +36,7 @@ class ThreadedMessageApplet : public Applet, public SinglePortModule explicit ThreadedMessageApplet(uint8_t channelIndex); ThreadedMessageApplet() = delete; - void onRender() override; + void onRender(bool full) override; void onActivate() override; void onDeactivate() override; diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp index cdda1638d..e6c16d350 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -55,10 +55,15 @@ void InkHUD::Events::onButtonShort() } // If no system applet is handling input, default behavior instead is to cycle applets - if (consumer) + // or open menu if joystick is enabled + if (consumer) { consumer->onButtonShortPress(); - else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module - inkhud->nextApplet(); + } else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module + if (!settings->joystick.enabled) + inkhud->nextApplet(); + else + inkhud->openMenu(); + } } void InkHUD::Events::onButtonLong() @@ -83,6 +88,189 @@ void InkHUD::Events::onButtonLong() inkhud->openMenu(); } +void InkHUD::Events::onExitShort() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + // If no system applet is handling input, default behavior instead is change tiles + if (consumer) + consumer->onExitShort(); + else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module + inkhud->nextTile(); + } +} + +void InkHUD::Events::onExitLong() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Slightly longer than playChirp + playBoop(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + if (consumer) + consumer->onExitLong(); + } +} + +void InkHUD::Events::onNavUp() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + if (consumer) + consumer->onNavUp(); + } +} + +void InkHUD::Events::onNavDown() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + if (consumer) + consumer->onNavDown(); + } +} + +void InkHUD::Events::onNavLeft() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + // If no system applet is handling input, default behavior instead is to cycle applets + if (consumer) + consumer->onNavLeft(); + else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module + inkhud->prevApplet(); + } +} + +void InkHUD::Events::onNavRight() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + // If no system applet is handling input, default behavior instead is to cycle applets + if (consumer) + consumer->onNavRight(); + else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module + inkhud->nextApplet(); + } +} + +void InkHUD::Events::onFreeText(char c) +{ + // Trigger the first system applet that wants to handle the new character + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleFreeText) { + sa->onFreeText(c); + break; + } + } +} + +void InkHUD::Events::onFreeTextDone() +{ + // Trigger the first system applet that wants to handle it + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleFreeText) { + sa->onFreeTextDone(); + break; + } + } +} + +void InkHUD::Events::onFreeTextCancel() +{ + // Trigger the first system applet that wants to handle it + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleFreeText) { + sa->onFreeTextCancel(); + break; + } + } +} + // Callback for deepSleepObserver // Returns 0 to signal that we agree to sleep now int InkHUD::Events::beforeDeepSleep(void *unused) @@ -111,7 +299,7 @@ int InkHUD::Events::beforeDeepSleep(void *unused) // then prepared a final powered-off screen for us, which shows device shortname. // We're updating to show that one now. - inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); + inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false); delay(1000); // Cooldown, before potentially yanking display power // InkHUD shutdown complete @@ -121,6 +309,15 @@ int InkHUD::Events::beforeDeepSleep(void *unused) return 0; // We agree: deep sleep now } +// Display an intermediate screen while configuration changes are applied +void InkHUD::Events::applyingChanges() +{ + // Bring the logo applet forward with a temporary message + for (SystemApplet *sa : inkhud->systemApplets) { + sa->onApplyingChanges(); + } +} + // Callback for rebootObserver // Same as shutdown, without drawing the logoApplet // Makes sure we don't lose message history / InkHUD config diff --git a/src/graphics/niche/InkHUD/Events.h b/src/graphics/niche/InkHUD/Events.h index df68f368c..873f53fd5 100644 --- a/src/graphics/niche/InkHUD/Events.h +++ b/src/graphics/niche/InkHUD/Events.h @@ -29,6 +29,18 @@ class Events void onButtonShort(); // User button: short press void onButtonLong(); // User button: long press + void applyingChanges(); + void onExitShort(); // Exit button: short press + void onExitLong(); // Exit button: long press + void onNavUp(); // Navigate up + void onNavDown(); // Navigate down + void onNavLeft(); // Navigate left + void onNavRight(); // Navigate right + + // Free text typing events + void onFreeText(char c); // New freetext character input + void onFreeTextDone(); + void onFreeTextCancel(); int beforeDeepSleep(void *unused); // Prepare for shutdown int beforeReboot(void *unused); // Prepare for reboot diff --git a/src/graphics/niche/InkHUD/InkHUD.cpp b/src/graphics/niche/InkHUD/InkHUD.cpp index 90b6718e0..5fab67639 100644 --- a/src/graphics/niche/InkHUD/InkHUD.cpp +++ b/src/graphics/niche/InkHUD/InkHUD.cpp @@ -53,6 +53,13 @@ void InkHUD::InkHUD::addApplet(const char *name, Applet *a, bool defaultActive, windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile); } +void InkHUD::InkHUD::notifyApplyingChanges() +{ + if (events) { + events->applyingChanges(); + } +} + // Start InkHUD! // Call this only after you have configured InkHUD void InkHUD::InkHUD::begin() @@ -80,6 +87,113 @@ void InkHUD::InkHUD::longpress() events->onButtonLong(); } +// Call this when your exit button gets a short press +void InkHUD::InkHUD::exitShort() +{ + events->onExitShort(); +} + +// Call this when your exit button gets a long press +void InkHUD::InkHUD::exitLong() +{ + events->onExitLong(); +} + +// Call this when your joystick gets an up input +void InkHUD::InkHUD::navUp() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavLeft(); + break; + case 2: // 180 deg + events->onNavDown(); + break; + case 3: // 270 deg + events->onNavRight(); + break; + default: // 0 deg + events->onNavUp(); + break; + } +} + +// Call this when your joystick gets a down input +void InkHUD::InkHUD::navDown() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavRight(); + break; + case 2: // 180 deg + events->onNavUp(); + break; + case 3: // 270 deg + events->onNavLeft(); + break; + default: // 0 deg + events->onNavDown(); + break; + } +} + +// Call this when your joystick gets a left input +void InkHUD::InkHUD::navLeft() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavDown(); + break; + case 2: // 180 deg + events->onNavRight(); + break; + case 3: // 270 deg + events->onNavUp(); + break; + default: // 0 deg + events->onNavLeft(); + break; + } +} + +// Call this when your joystick gets a right input +void InkHUD::InkHUD::navRight() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavUp(); + break; + case 2: // 180 deg + events->onNavLeft(); + break; + case 3: // 270 deg + events->onNavDown(); + break; + default: // 0 deg + events->onNavRight(); + break; + } +} + +// Call this for keyboard input +// The Keyboard Applet also calls this +void InkHUD::InkHUD::freeText(char c) +{ + events->onFreeText(c); +} + +// Call this to complete a freetext input +void InkHUD::InkHUD::freeTextDone() +{ + events->onFreeTextDone(); +} + +// Call this to cancel a freetext input +void InkHUD::InkHUD::freeTextCancel() +{ + events->onFreeTextCancel(); +} + // Cycle the next user applet to the foreground // Only activated applets are cycled // If user has a multi-applet layout, the applets will cycle on the "focused tile" @@ -88,6 +202,14 @@ void InkHUD::InkHUD::nextApplet() windowManager->nextApplet(); } +// Cycle the previous user applet to the foreground +// Only activated applets are cycled +// If user has a multi-applet layout, the applets will cycle on the "focused tile" +void InkHUD::InkHUD::prevApplet() +{ + windowManager->prevApplet(); +} + // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes void InkHUD::InkHUD::openMenu() @@ -95,6 +217,24 @@ void InkHUD::InkHUD::openMenu() windowManager->openMenu(); } +// Bring AlignStick applet to the foreground +void InkHUD::InkHUD::openAlignStick() +{ + windowManager->openAlignStick(); +} + +// Open the on-screen keyboard +void InkHUD::InkHUD::openKeyboard() +{ + windowManager->openKeyboard(); +} + +// Close the on-screen keyboard +void InkHUD::InkHUD::closeKeyboard() +{ + windowManager->closeKeyboard(); +} + // In layouts where multiple applets are shown at once, change which tile is focused // The focused tile in the one which cycles applets on button short press, and displays menu on long press void InkHUD::InkHUD::nextTile() @@ -102,12 +242,26 @@ void InkHUD::InkHUD::nextTile() windowManager->nextTile(); } +// In layouts where multiple applets are shown at once, change which tile is focused +// The focused tile in the one which cycles applets on button short press, and displays menu on long press +void InkHUD::InkHUD::prevTile() +{ + windowManager->prevTile(); +} + // Rotate the display image by 90 degrees void InkHUD::InkHUD::rotate() { windowManager->rotate(); } +// rotate the joystick in 90 degree increments +void InkHUD::InkHUD::rotateJoystick(uint8_t angle) +{ + persistence->settings.joystick.alignment += angle; + persistence->settings.joystick.alignment %= 4; +} + // Show / hide the battery indicator in top-right void InkHUD::InkHUD::toggleBatteryIcon() { @@ -129,10 +283,11 @@ void InkHUD::InkHUD::requestUpdate() // Ignores all diplomacy: // - the display *will* update // - the specified update type *will* be used +// If the all parameter is true, the whole screen buffer is cleared and re-rendered // If the async parameter is false, code flow is blocked while the update takes place -void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool async) +void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool all, bool async) { - renderer->forceUpdate(type, async); + renderer->forceUpdate(type, all, async); } // Wait for any in-progress display update to complete before continuing diff --git a/src/graphics/niche/InkHUD/InkHUD.h b/src/graphics/niche/InkHUD/InkHUD.h index 13839ea22..ae029137e 100644 --- a/src/graphics/niche/InkHUD/InkHUD.h +++ b/src/graphics/niche/InkHUD/InkHUD.h @@ -47,6 +47,7 @@ class InkHUD void setDriver(Drivers::EInk *driver); void setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0); void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, uint8_t onTile = -1); + void notifyApplyingChanges(); void begin(); @@ -55,22 +56,43 @@ class InkHUD void shortpress(); void longpress(); + void exitShort(); + void exitLong(); + void navUp(); + void navDown(); + void navLeft(); + void navRight(); + + // Freetext handlers + void freeText(char c); + void freeTextDone(); + void freeTextCancel(); // Trigger UI changes // - called by various InkHUD components // - suitable(?) for use by aux button, connected in variant nicheGraphics.h void nextApplet(); + void prevApplet(); void openMenu(); + void openAlignStick(); + void openKeyboard(); + void closeKeyboard(); void nextTile(); + void prevTile(); void rotate(); + void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default void toggleBatteryIcon(); + // Used by TipsApplet to force menu to start on Region selection + bool forceRegionMenu = false; + // Updating the display // - called by various InkHUD components void requestUpdate(); - void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool async = true); + void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool all = false, + bool async = true); void awaitUpdate(); // (Re)configuring WindowManager diff --git a/src/graphics/niche/InkHUD/Persistence.h b/src/graphics/niche/InkHUD/Persistence.h index b85274c87..5054b7234 100644 --- a/src/graphics/niche/InkHUD/Persistence.h +++ b/src/graphics/niche/InkHUD/Persistence.h @@ -29,7 +29,7 @@ class Persistence // Used to invalidate old settings, if needed // Version 0 is reserved for testing, and will always load defaults - static constexpr uint32_t SETTINGS_VERSION = 2; + static constexpr uint32_t SETTINGS_VERSION = 3; struct Settings { struct Meta { @@ -96,6 +96,19 @@ class Persistence bool safeShutdownSeen = false; } tips; + // Joystick settings for enabling and aligning to the screen + struct Joystick { + // Modifies the UI for joystick use + bool enabled = false; + + // gets set to true when AlignStick applet is completed + bool aligned = false; + + // Rotation of the joystick + // Multiples of 90 degrees clockwise + uint8_t alignment = 0; + } joystick; + // Rotation of the display // Multiples of 90 degrees clockwise // Most commonly: rotation is 0 when flex connector is oriented below display diff --git a/src/graphics/niche/InkHUD/PlatformioConfig.ini b/src/graphics/niche/InkHUD/PlatformioConfig.ini index 80984f399..b985f9f77 100644 --- a/src/graphics/niche/InkHUD/PlatformioConfig.ini +++ b/src/graphics/niche/InkHUD/PlatformioConfig.ini @@ -8,4 +8,5 @@ build_flags = -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 + # TODO renovate + https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX diff --git a/src/graphics/niche/InkHUD/Renderer.cpp b/src/graphics/niche/InkHUD/Renderer.cpp index 072e9dbd6..89a83c932 100644 --- a/src/graphics/niche/InkHUD/Renderer.cpp +++ b/src/graphics/niche/InkHUD/Renderer.cpp @@ -56,15 +56,16 @@ void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMul void InkHUD::Renderer::begin() { - forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); + forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false); } // Set a flag, which will be picked up by runOnce, ASAP. // Quite likely, multiple applets will all want to respond to one event (Observable, etc) // Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next runOnce -void InkHUD::Renderer::requestUpdate() +void InkHUD::Renderer::requestUpdate(bool all) { requested = true; + renderAll |= all; // We will run the thread as soon as we loop(), // after all Applets have had a chance to observe whatever event set this off @@ -79,10 +80,11 @@ void InkHUD::Renderer::requestUpdate() // Sometimes, however, we will want to trigger a display update manually, in the absence of any sort of applet event // Display health, for example. // In these situations, we use forceUpdate -void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool async) +void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool all, bool async) { requested = true; forced = true; + renderAll |= all; displayHealth.forceUpdateType(type); // Normally, we need to start the timer, in case the display is busy and we briefly defer the update @@ -219,7 +221,8 @@ void InkHUD::Renderer::render(bool async) Drivers::EInk::UpdateTypes updateType = decideUpdateType(); // Render the new image - clearBuffer(); + if (renderAll) + clearBuffer(); renderUserApplets(); renderPlaceholders(); renderSystemApplets(); @@ -247,6 +250,7 @@ void InkHUD::Renderer::render(bool async) // Tidy up, ready for a new request requested = false; forced = false; + renderAll = false; } // Manually fill the image buffer with WHITE @@ -259,6 +263,76 @@ void InkHUD::Renderer::clearBuffer() memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth); } +// Manually clear the pixels below a tile +void InkHUD::Renderer::clearTile(Tile *t) +{ + // Rotate the tile dimensions + int16_t left = 0; + int16_t top = 0; + uint16_t width = 0; + uint16_t height = 0; + switch (settings->rotation) { + case 0: + left = t->getLeft(); + top = t->getTop(); + width = t->getWidth(); + height = t->getHeight(); + break; + case 1: + left = driver->width - (t->getTop() + t->getHeight()); + top = t->getLeft(); + width = t->getHeight(); + height = t->getWidth(); + break; + case 2: + left = driver->width - (t->getLeft() + t->getWidth()); + top = driver->height - (t->getTop() + t->getHeight()); + width = t->getWidth(); + height = t->getHeight(); + break; + case 3: + left = t->getTop(); + top = driver->height - (t->getLeft() + t->getWidth()); + width = t->getHeight(); + height = t->getWidth(); + break; + } + + // Calculate the bounds to clear + uint16_t xStart = (left < 0) ? 0 : left; + uint16_t yStart = (top < 0) ? 0 : top; + if (xStart >= driver->width || yStart >= driver->height || left + width < 0 || top + height < 0) + return; // the box is completely off the screen + uint16_t xEnd = left + width; + uint16_t yEnd = top + height; + if (xEnd > driver->width) + xEnd = driver->width; + if (yEnd > driver->height) + yEnd = driver->height; + + // Clear the pixels + if (xStart == 0 && xEnd == driver->width) { // full width box is easier to clear + memset(imageBuffer + (yStart * imageBufferWidth), 0xFF, (yEnd - yStart) * imageBufferWidth); + } else { + const uint16_t byteStart = (xStart / 8) + 1; + const uint16_t byteEnd = xEnd / 8; + const uint8_t leadingByte = 0xFF >> (xStart - ((byteStart - 1) * 8)); + const uint8_t trailingByte = (0xFF00 >> (xEnd - (byteEnd * 8))) & 0xFF; + for (uint16_t i = yStart * imageBufferWidth; i < yEnd * imageBufferWidth; i += imageBufferWidth) { + // Set the leading byte + imageBuffer[i + byteStart - 1] |= leadingByte; + + // Set the continuous bytes + if (byteStart < byteEnd) + memset(imageBuffer + i + byteStart, 0xFF, byteEnd - byteStart); + + // Set the trailing byte + if (byteEnd != imageBufferWidth) + imageBuffer[i + byteEnd] |= trailingByte; + } + } +} + void InkHUD::Renderer::checkLocks() { lockRendering = nullptr; @@ -323,12 +397,12 @@ Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType() if (!forced) { // User applets for (Applet *ua : inkhud->userApplets) { - if (ua && ua->isForeground()) + if (ua && ua->isForeground() && (ua->wantsToRender() || renderAll)) displayHealth.requestUpdateType(ua->wantsUpdateType()); } // System Applets for (SystemApplet *sa : inkhud->systemApplets) { - if (sa && sa->isForeground()) + if (sa && sa->isForeground() && (sa->wantsToRender() || sa->alwaysRender || renderAll)) displayHealth.requestUpdateType(sa->wantsUpdateType()); } } @@ -346,9 +420,16 @@ void InkHUD::Renderer::renderUserApplets() // Render any user applets which are currently visible for (Applet *ua : inkhud->userApplets) { - if (ua && ua->isActive() && ua->isForeground()) { + if (ua && ua->isActive() && ua->isForeground() && (ua->wantsToRender() || renderAll)) { + + // Clear the tile unless the applet wants to draw over its previous render + // or everything is getting re-rendered anyways + if (ua->wantsFullRender() && !renderAll) + clearTile(ua->getTile()); + uint32_t start = millis(); - ua->render(); // Draw! + bool full = ua->wantsFullRender() || renderAll; + ua->render(full); // Draw! uint32_t stop = millis(); LOG_DEBUG("%s took %dms to render", ua->name, stop - start); } @@ -370,6 +451,9 @@ void InkHUD::Renderer::renderSystemApplets() if (!sa->isForeground()) continue; + if (!sa->wantsToRender() && !sa->alwaysRender && !renderAll) + continue; + // Skip if locked by another applet if (lockRendering && lockRendering != sa) continue; @@ -381,8 +465,14 @@ void InkHUD::Renderer::renderSystemApplets() assert(sa->getTile()); + // Clear the tile unless the applet wants to draw over its previous render + // or everything is getting re-rendered anyways + if (sa->wantsFullRender() && !renderAll) + clearTile(sa->getTile()); + // uint32_t start = millis(); - sa->render(); // Draw! + bool full = sa->wantsFullRender() || renderAll; + sa->render(full); // Draw! // uint32_t stop = millis(); // LOG_DEBUG("%s took %dms to render", sa->name, stop - start); } @@ -409,7 +499,10 @@ void InkHUD::Renderer::renderPlaceholders() // uint32_t start = millis(); for (Tile *t : emptyTiles) { t->assignApplet(placeholder); - placeholder->render(); + // Clear the tile unless everything is getting re-rendered + if (!renderAll) + clearTile(t); + placeholder->render(true); // full render t->assignApplet(nullptr); } // uint32_t stop = millis(); diff --git a/src/graphics/niche/InkHUD/Renderer.h b/src/graphics/niche/InkHUD/Renderer.h index b6cf9e215..5cfb79277 100644 --- a/src/graphics/niche/InkHUD/Renderer.h +++ b/src/graphics/niche/InkHUD/Renderer.h @@ -37,8 +37,8 @@ class Renderer : protected concurrency::OSThread // Call these to make the image change - void requestUpdate(); // Update display, if a foreground applet has info it wants to show - void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, + void requestUpdate(bool all = false); // Update display, if a foreground applet has info it wants to show + void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool all = false, bool async = true); // Update display, regardless of whether any applets requested this // Wait for an update to complete @@ -65,6 +65,7 @@ class Renderer : protected concurrency::OSThread // Steps of the rendering process void clearBuffer(); + void clearTile(Tile *t); void checkLocks(); bool shouldUpdate(); Drivers::EInk::UpdateTypes decideUpdateType(); @@ -85,6 +86,7 @@ class Renderer : protected concurrency::OSThread bool requested = false; bool forced = false; + bool renderAll = false; // For convenience InkHUD *inkhud = nullptr; diff --git a/src/graphics/niche/InkHUD/SystemApplet.h b/src/graphics/niche/InkHUD/SystemApplet.h index 7ee47eeb9..32e0e58bb 100644 --- a/src/graphics/niche/InkHUD/SystemApplet.h +++ b/src/graphics/niche/InkHUD/SystemApplet.h @@ -22,11 +22,14 @@ class SystemApplet : public Applet public: // System applets have the right to: - bool handleInput = false; // - respond to input from the user button - bool lockRendering = false; // - prevent other applets from being rendered during an update - bool lockRequests = false; // - prevent other applets from triggering display updates + bool handleInput = false; // - respond to input from the user button + bool handleFreeText = false; // - respond to free text input + bool lockRendering = false; // - prevent other applets from being rendered during an update + bool lockRequests = false; // - prevent other applets from triggering display updates + bool alwaysRender = false; // - render every time the screen is updated virtual void onReboot() { onShutdown(); } // - handle reboot specially + virtual void onApplyingChanges() {} // Other system applets may take precedence over our own system applet though // The order an applet is passed to WindowManager::addSystemApplet determines this hierarchy (added earlier = higher rank) @@ -40,4 +43,4 @@ class SystemApplet : public Applet }; // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Tile.cpp b/src/graphics/niche/InkHUD/Tile.cpp index 5e548de74..8beb25f39 100644 --- a/src/graphics/niche/InkHUD/Tile.cpp +++ b/src/graphics/niche/InkHUD/Tile.cpp @@ -18,7 +18,7 @@ static int32_t runtaskHighlight() LOG_DEBUG("Dismissing Highlight"); InkHUD::Tile::highlightShown = false; InkHUD::Tile::highlightTarget = nullptr; - InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting + InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true); // Re-render, clearing the highlighting return taskHighlight->disable(); } static void inittaskHighlight() @@ -190,6 +190,18 @@ void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c) } } +// Used in Renderer for clearing the tile +int16_t InkHUD::Tile::getLeft() +{ + return left; +} + +// Used in Renderer for clearing the tile +int16_t InkHUD::Tile::getTop() +{ + return top; +} + // Called by Applet base class, when setting applet dimensions, immediately before render uint16_t InkHUD::Tile::getWidth() { @@ -220,7 +232,7 @@ void InkHUD::Tile::requestHighlight() { Tile::highlightTarget = this; Tile::highlightShown = false; - inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST); + inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true); } // Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first diff --git a/src/graphics/niche/InkHUD/Tile.h b/src/graphics/niche/InkHUD/Tile.h index 0f5444f17..0c09e4704 100644 --- a/src/graphics/niche/InkHUD/Tile.h +++ b/src/graphics/niche/InkHUD/Tile.h @@ -29,6 +29,8 @@ class Tile void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout void setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet + int16_t getLeft(); + int16_t getTop(); uint16_t getWidth(); uint16_t getHeight(); static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter diff --git a/src/graphics/niche/InkHUD/WindowManager.cpp b/src/graphics/niche/InkHUD/WindowManager.cpp index c883e9a29..9c18fbd48 100644 --- a/src/graphics/niche/InkHUD/WindowManager.cpp +++ b/src/graphics/niche/InkHUD/WindowManager.cpp @@ -2,7 +2,9 @@ #include "./WindowManager.h" +#include "./Applets/System/AlignStick/AlignStickApplet.h" #include "./Applets/System/BatteryIcon/BatteryIconApplet.h" +#include "./Applets/System/Keyboard/KeyboardApplet.h" #include "./Applets/System/Logo/LogoApplet.h" #include "./Applets/System/Menu/MenuApplet.h" #include "./Applets/System/Notification/NotificationApplet.h" @@ -98,6 +100,38 @@ void InkHUD::WindowManager::nextTile() userTiles.at(settings->userTiles.focused)->requestHighlight(); } +// Focus on a different tile but decrement index +void InkHUD::WindowManager::prevTile() +{ + // Close the menu applet if open + // We don't *really* want to do this, but it simplifies handling *a lot* + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); + bool menuWasOpen = false; + if (menu->isForeground()) { + menu->sendToBackground(); + menuWasOpen = true; + } + + // Swap to next tile + if (settings->userTiles.focused == 0) + settings->userTiles.focused = settings->userTiles.count - 1; + else + settings->userTiles.focused--; + + // Make sure that we don't get stuck on the placeholder tile + refocusTile(); + + if (menuWasOpen) + menu->show(userTiles.at(settings->userTiles.focused)); + + // Ask the tile to draw an indicator showing which tile is now focused + // Requests a render + // We only draw this indicator if the device uses an aux button to switch tiles. + // Assume aux button is used to switch tiles if the "next tile" menu item is hidden + if (!settings->optionalMenuItems.nextTile) + userTiles.at(settings->userTiles.focused)->requestHighlight(); +} + // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes void InkHUD::WindowManager::openMenu() @@ -106,6 +140,37 @@ void InkHUD::WindowManager::openMenu() menu->show(userTiles.at(settings->userTiles.focused)); } +// Bring the AlignStick applet to the foreground +void InkHUD::WindowManager::openAlignStick() +{ + if (settings->joystick.enabled) { + AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick"); + alignStick->bringToForeground(); + } +} + +void InkHUD::WindowManager::openKeyboard() +{ + KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard"); + + if (keyboard) { + keyboard->bringToForeground(); + keyboardOpen = true; + changeLayout(); + } +} + +void InkHUD::WindowManager::closeKeyboard() +{ + KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard"); + + if (keyboard) { + keyboard->sendToBackground(); + keyboardOpen = false; + changeLayout(); + } +} + // On the currently focussed tile: cycle to the next available user applet // Applets available for this must be activated, and not already displayed on another tile void InkHUD::WindowManager::nextApplet() @@ -155,6 +220,59 @@ void InkHUD::WindowManager::nextApplet() inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST } +// On the currently focussed tile: cycle to the previous available user applet +// Applets available for this must be activated, and not already displayed on another tile +void InkHUD::WindowManager::prevApplet() +{ + Tile *t = userTiles.at(settings->userTiles.focused); + + // Abort if zero applets available + // nullptr means WindowManager::refocusTile determined that there were no available applets + if (!t->getAssignedApplet()) + return; + + // Find the index of the applet currently shown on the tile + uint8_t appletIndex = -1; + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + if (inkhud->userApplets.at(i) == t->getAssignedApplet()) { + appletIndex = i; + break; + } + } + + // Confirm that we did find the applet + assert(appletIndex != (uint8_t)-1); + + // Iterate forward through the WindowManager::applets, looking for the previous valid applet + Applet *prevValidApplet = nullptr; + for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) { + uint8_t newAppletIndex = 0; + if (i > appletIndex) + newAppletIndex = inkhud->userApplets.size() + appletIndex - i; + else + newAppletIndex = (appletIndex - i); + Applet *a = inkhud->userApplets.at(newAppletIndex); + + // Looking for an applet which is active (enabled by user), but currently in background + if (a->isActive() && !a->isForeground()) { + prevValidApplet = a; + settings->userTiles.displayedUserApplet[settings->userTiles.focused] = + newAppletIndex; // Remember this setting between boots! + break; + } + } + + // Confirm that we found another applet + if (!prevValidApplet) + return; + + // Hide old applet, show new applet + t->getAssignedApplet()->sendToBackground(); + t->assignApplet(prevValidApplet); + prevValidApplet->bringToForeground(); + inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST +} + // Rotate the display image by 90 degrees void InkHUD::WindowManager::rotate() { @@ -177,7 +295,6 @@ void InkHUD::WindowManager::toggleBatteryIcon() batteryIcon->sendToBackground(); // Force-render - // - redraw all applets inkhud->forceUpdate(EInk::UpdateTypes::FAST); } @@ -216,9 +333,25 @@ void InkHUD::WindowManager::changeLayout() menu->show(ft); } + // Resize for the on-screen keyboard + if (keyboardOpen) { + // Send all user applets to the background + // User applets currently don't handle free text input + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) + inkhud->userApplets.at(i)->sendToBackground(); + // Find the first system applet that can handle freetext and resize it + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleFreeText) { + const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight(); + sa->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height() - keyboardHeight - 1); + break; + } + } + } + // Force-render // - redraw all applets - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + inkhud->forceUpdate(EInk::UpdateTypes::FAST, true); } // Perform necessary reconfiguration when user activates or deactivates applets at run-time @@ -252,7 +385,7 @@ void InkHUD::WindowManager::changeActivatedApplets() // Force-render // - redraw all applets - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + inkhud->forceUpdate(EInk::UpdateTypes::FAST, true); } // Some applets may be permitted to bring themselves to foreground, to show new data @@ -338,6 +471,10 @@ void InkHUD::WindowManager::createSystemApplets() addSystemApplet("Logo", new LogoApplet, new Tile); addSystemApplet("Pairing", new PairingApplet, new Tile); addSystemApplet("Tips", new TipsApplet, new Tile); + if (settings->joystick.enabled) { + addSystemApplet("AlignStick", new AlignStickApplet, new Tile); + addSystemApplet("Keyboard", new KeyboardApplet, new Tile); + } addSystemApplet("Menu", new MenuApplet, nullptr); @@ -360,7 +497,13 @@ void InkHUD::WindowManager::placeSystemTiles() inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); - + if (settings->joystick.enabled) { + inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); + const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight(); + inkhud->getSystemApplet("Keyboard") + ->getTile() + ->setRegion(0, inkhud->height() - keyboardHeight, inkhud->width(), keyboardHeight); + } inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20); const uint16_t batteryIconHeight = Applet::getHeaderHeight() - 2 - 2; diff --git a/src/graphics/niche/InkHUD/WindowManager.h b/src/graphics/niche/InkHUD/WindowManager.h index 4d1aedf1b..948ef6131 100644 --- a/src/graphics/niche/InkHUD/WindowManager.h +++ b/src/graphics/niche/InkHUD/WindowManager.h @@ -28,8 +28,13 @@ class WindowManager // - call these to make stuff change void nextTile(); + void prevTile(); void openMenu(); + void openAlignStick(); + void openKeyboard(); + void closeKeyboard(); void nextApplet(); + void prevApplet(); void rotate(); void toggleBatteryIcon(); @@ -61,6 +66,7 @@ class WindowManager void findOrphanApplets(); // Find any applets left-behind when layout changes std::vector userTiles; // Tiles which can host user applets + bool keyboardOpen = false; // For convenience InkHUD *inkhud = nullptr; diff --git a/src/graphics/niche/InkHUD/docs/README.md b/src/graphics/niche/InkHUD/docs/README.md index e7821299e..8c30aba58 100644 --- a/src/graphics/niche/InkHUD/docs/README.md +++ b/src/graphics/niche/InkHUD/docs/README.md @@ -174,7 +174,7 @@ class BasicExampleApplet : public Applet // You must have an onRender() method // All drawing happens here - void onRender() override; + void onRender(bool full) override; }; ``` @@ -183,7 +183,7 @@ The `onRender` method is called when the display image is redrawn. This can happ ```cpp // All drawing happens here // Our basic example doesn't do anything useful. It just passively prints some text. -void InkHUD::BasicExampleApplet::onRender() +void InkHUD::BasicExampleApplet::onRender(bool full) { printAt(0, 0, "Hello, world!"); } @@ -273,7 +273,7 @@ _(Example shows only config required by InkHUD. This is not a complete `env` def extends = esp32s3_base, inkhud ; or nrf52840_base, etc build_src_filter = -${esp32_base.build_src_filter} +${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = @@ -756,12 +756,12 @@ This mapping of emoji to control characters is fairly arbitrary. Selection was i | `0x03` | 🙂 | | `0x04` | 😆 | | `0x05` | 👋 | -| `0x06` | ☀ | +| `0x06` | ☀ | | ~~`0x07`~~ | (bell char, unused) | | `0x08` | 🌧 | -| `0x09` | ☁ | +| `0x09` | ☁ | | ~~`0x0A`~~ | (line feed, unused) | -| `0x0B` | ♥ | +| `0x0B` | ♥ | | `0x0C` | 💩 | | ~~`0x0D`~~ | (carriage return, unused) | | `0x0E` | 🔔 | diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.cpp b/src/graphics/niche/Inputs/TwoButtonExtended.cpp new file mode 100644 index 000000000..287fb943f --- /dev/null +++ b/src/graphics/niche/Inputs/TwoButtonExtended.cpp @@ -0,0 +1,523 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "./TwoButtonExtended.h" + +#include "NodeDB.h" // For the helper function TwoButtonExtended::getUserButtonPin +#include "PowerFSM.h" +#include "sleep.h" + +using namespace NicheGraphics::Inputs; + +TwoButtonExtended::TwoButtonExtended() : concurrency::OSThread("TwoButtonExtended") +{ + // Don't start polling buttons for release immediately + // Assume they are in a "released" state at boot + OSThread::disable(); + +#ifdef ARCH_ESP32 + // Register callbacks for before and after lightsleep + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); +#endif + + // Explicitly initialize these, just to keep cppcheck quiet.. + buttons[0] = Button(); + buttons[1] = Button(); + joystick[Direction::UP] = SimpleButton(); + joystick[Direction::DOWN] = SimpleButton(); + joystick[Direction::LEFT] = SimpleButton(); + joystick[Direction::RIGHT] = SimpleButton(); +} + +// Get access to (or create) the singleton instance of this class +// Accessible inside the ISRs, even though we maybe shouldn't +TwoButtonExtended *TwoButtonExtended::getInstance() +{ + // Instantiate the class the first time this method is called + static TwoButtonExtended *const singletonInstance = new TwoButtonExtended; + + return singletonInstance; +} + +// Begin receiving button input +// We probably need to do this after sleep, as well as at boot +void TwoButtonExtended::start() +{ + if (buttons[0].pin != 0xFF) + attachInterrupt(buttons[0].pin, TwoButtonExtended::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING); + + if (buttons[1].pin != 0xFF) + attachInterrupt(buttons[1].pin, TwoButtonExtended::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING); + + if (joystick[Direction::UP].pin != 0xFF) + attachInterrupt(joystick[Direction::UP].pin, TwoButtonExtended::isrJoystickUp, + joystickActiveLogic == LOW ? FALLING : RISING); + + if (joystick[Direction::DOWN].pin != 0xFF) + attachInterrupt(joystick[Direction::DOWN].pin, TwoButtonExtended::isrJoystickDown, + joystickActiveLogic == LOW ? FALLING : RISING); + + if (joystick[Direction::LEFT].pin != 0xFF) + attachInterrupt(joystick[Direction::LEFT].pin, TwoButtonExtended::isrJoystickLeft, + joystickActiveLogic == LOW ? FALLING : RISING); + + if (joystick[Direction::RIGHT].pin != 0xFF) + attachInterrupt(joystick[Direction::RIGHT].pin, TwoButtonExtended::isrJoystickRight, + joystickActiveLogic == LOW ? FALLING : RISING); +} + +// Stop receiving button input, and run custom sleep code +// Called before device sleeps. This might be power-off, or just ESP32 light sleep +// Some devices will want to attach interrupts here, for the user button to wake from sleep +void TwoButtonExtended::stop() +{ + if (buttons[0].pin != 0xFF) + detachInterrupt(buttons[0].pin); + + if (buttons[1].pin != 0xFF) + detachInterrupt(buttons[1].pin); + + if (joystick[Direction::UP].pin != 0xFF) + detachInterrupt(joystick[Direction::UP].pin); + + if (joystick[Direction::DOWN].pin != 0xFF) + detachInterrupt(joystick[Direction::DOWN].pin); + + if (joystick[Direction::LEFT].pin != 0xFF) + detachInterrupt(joystick[Direction::LEFT].pin); + + if (joystick[Direction::RIGHT].pin != 0xFF) + detachInterrupt(joystick[Direction::RIGHT].pin); +} + +// Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings +// This helper method isn't used by the TwoButtonExtended class itself, it could be moved elsewhere. +// Intention is to pass this value to TwoButtonExtended::setWiring in the setupNicheGraphics method. +uint8_t TwoButtonExtended::getUserButtonPin() +{ + uint8_t pin = 0xFF; // Unset + + // Use default pin for variant, if no better source +#ifdef BUTTON_PIN + pin = BUTTON_PIN; +#endif + + // From userPrefs.jsonc, if set +#ifdef USERPREFS_BUTTON_PIN + pin = USERPREFS_BUTTON_PIN; +#endif + + // From user's override in device settings, if set + if (config.device.button_gpio) + pin = config.device.button_gpio; + + return pin; +} + +// Configures the wiring and logic of either button +// Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp +void TwoButtonExtended::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) +{ + // Prevent the same GPIO being assigned to multiple buttons + // Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button + for (uint8_t i = 0; i < whichButton; i++) { + if (buttons[i].pin == pin) { + LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton); + return; + } + } + + assert(whichButton < 2); + buttons[whichButton].pin = pin; + buttons[whichButton].activeLogic = LOW; + + pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT); +} + +// Configures the wiring and logic of the joystick buttons +// Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp +void TwoButtonExtended::setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup) +{ + if (joystick[Direction::UP].pin == uPin || joystick[Direction::DOWN].pin == dPin || joystick[Direction::LEFT].pin == lPin || + joystick[Direction::RIGHT].pin == rPin) { + LOG_WARN("Attempted reuse of Joystick GPIO. Ignoring assignment"); + return; + } + + joystick[Direction::UP].pin = uPin; + joystick[Direction::DOWN].pin = dPin; + joystick[Direction::LEFT].pin = lPin; + joystick[Direction::RIGHT].pin = rPin; + joystickActiveLogic = LOW; + + pinMode(joystick[Direction::UP].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::DOWN].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::LEFT].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT); +} + +void TwoButtonExtended::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) +{ + assert(whichButton < 2); + buttons[whichButton].debounceLength = debounceMs; + buttons[whichButton].longpressLength = longpressMs; +} + +void TwoButtonExtended::setJoystickDebounce(uint32_t debounceMs) +{ + joystickDebounceLength = debounceMs; +} + +// Set what should happen when a button becomes pressed +// Use this to implement a "while held" behavior +void TwoButtonExtended::setHandlerDown(uint8_t whichButton, Callback onDown) +{ + assert(whichButton < 2); + buttons[whichButton].onDown = onDown; +} + +// Set what should happen when a button becomes unpressed +// Use this to implement a "While held" behavior +void TwoButtonExtended::setHandlerUp(uint8_t whichButton, Callback onUp) +{ + assert(whichButton < 2); + buttons[whichButton].onUp = onUp; +} + +// Set what should happen when a "short press" event has occurred +void TwoButtonExtended::setHandlerShortPress(uint8_t whichButton, Callback onPress) +{ + assert(whichButton < 2); + buttons[whichButton].onPress = onPress; +} + +// Set what should happen when a "long press" event has fired +// Note: this will occur while the button is still held +void TwoButtonExtended::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) +{ + assert(whichButton < 2); + buttons[whichButton].onLongPress = onLongPress; +} + +// Set what should happen when a joystick button becomes pressed +// Use this to implement a "while held" behavior +void TwoButtonExtended::setJoystickDownHandlers(Callback uDown, Callback dDown, Callback lDown, Callback rDown) +{ + joystick[Direction::UP].onDown = uDown; + joystick[Direction::DOWN].onDown = dDown; + joystick[Direction::LEFT].onDown = lDown; + joystick[Direction::RIGHT].onDown = rDown; +} + +// Set what should happen when a joystick button becomes unpressed +// Use this to implement a "while held" behavior +void TwoButtonExtended::setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp) +{ + joystick[Direction::UP].onUp = uUp; + joystick[Direction::DOWN].onUp = dUp; + joystick[Direction::LEFT].onUp = lUp; + joystick[Direction::RIGHT].onUp = rUp; +} + +// Set what should happen when a "press" event has fired +// Note: this will occur while the joystick button is still held +void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress) +{ + joystick[Direction::UP].onPress = uPress; + joystick[Direction::DOWN].onPress = dPress; + joystick[Direction::LEFT].onPress = lPress; + joystick[Direction::RIGHT].onPress = rPress; +} + +// Handle the start of a press to the primary button +// Wakes our button thread +void TwoButtonExtended::isrPrimary() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->buttons[0].state == State::REST) { + b->buttons[0].state = State::IRQ; + b->buttons[0].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +// Handle the start of a press to the secondary button +// Wakes our button thread +void TwoButtonExtended::isrSecondary() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->buttons[1].state == State::REST) { + b->buttons[1].state = State::IRQ; + b->buttons[1].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +// Handle the start of a press to the joystick buttons +// Also wakes our button thread +void TwoButtonExtended::isrJoystickUp() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::UP].state == State::REST) { + b->joystick[Direction::UP].state = State::IRQ; + b->joystick[Direction::UP].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +void TwoButtonExtended::isrJoystickDown() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::DOWN].state == State::REST) { + b->joystick[Direction::DOWN].state = State::IRQ; + b->joystick[Direction::DOWN].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +void TwoButtonExtended::isrJoystickLeft() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::LEFT].state == State::REST) { + b->joystick[Direction::LEFT].state = State::IRQ; + b->joystick[Direction::LEFT].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +void TwoButtonExtended::isrJoystickRight() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::RIGHT].state == State::REST) { + b->joystick[Direction::RIGHT].state = State::IRQ; + b->joystick[Direction::RIGHT].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +// Concise method to start our button thread +// Follows an ISR, listening for button release +void TwoButtonExtended::startThread() +{ + if (!OSThread::enabled) { + OSThread::setInterval(10); + OSThread::enabled = true; + } +} + +// Concise method to stop our button thread +// Called when we no longer need to poll for button release +void TwoButtonExtended::stopThread() +{ + if (OSThread::enabled) { + OSThread::disable(); + } + + // Reset both buttons manually + // Just in case an IRQ fires during the process of resetting the system + // Can occur with super rapid presses? + buttons[0].state = REST; + buttons[1].state = REST; + joystick[Direction::UP].state = REST; + joystick[Direction::DOWN].state = REST; + joystick[Direction::LEFT].state = REST; + joystick[Direction::RIGHT].state = REST; +} + +// Our button thread +// Started by an IRQ, on either button +// Polls for button releases +// Stops when both buttons released +int32_t TwoButtonExtended::runOnce() +{ + constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button); + constexpr uint8_t JOYSTICK_COUNT = sizeof(joystick) / sizeof(SimpleButton); + + // Allow either button to request that our thread should continue polling + bool awaitingRelease = false; + + // Check both primary and secondary buttons + for (uint8_t i = 0; i < BUTTON_COUNT; i++) { + switch (buttons[i].state) { + // No action: button has not been pressed + case REST: + break; + + // New press detected by interrupt + case IRQ: + powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) + buttons[i].onDown(); // Run callback: press has begun (possible hold behavior) + buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled + awaitingRelease = true; // Mark that polling-for-release should continue + break; + + // An existing press continues + // Not held long enough to register as longpress + case POLLING_UNFIRED: { + uint32_t length = millis() - buttons[i].irqAtMillis; + + // If button released since last thread tick, + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].onUp(); // Run callback: press has ended (possible release of a hold) + buttons[i].state = State::REST; // Mark that the button has reset + if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress, + buttons[i].onPress(); // Run callback: press + } + // If button not yet released + else { + awaitingRelease = true; // Mark that polling-for-release should continue + if (length >= buttons[i].longpressLength) { + // Run callback: long press (once) + // Then continue waiting for release, to rearm + buttons[i].state = State::POLLING_FIRED; + buttons[i].onLongPress(); + } + } + break; + } + + // Button still held, but duration long enough that longpress event already fired + // Just waiting for release + case POLLING_FIRED: + // Release detected + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].state = State::REST; + buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired) + } + // Not yet released, keep polling + else + awaitingRelease = true; + break; + } + } + + // Check all the joystick directions + for (uint8_t i = 0; i < JOYSTICK_COUNT; i++) { + switch (joystick[i].state) { + // No action: button has not been pressed + case REST: + break; + + // New press detected by interrupt + case IRQ: + powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) + joystick[i].onDown(); // Run callback: press has begun (possible hold behavior) + joystick[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled + awaitingRelease = true; // Mark that polling-for-release should continue + break; + + // An existing press continues + // Not held long enough to register as press + case POLLING_UNFIRED: { + uint32_t length = millis() - joystick[i].irqAtMillis; + + // If button released since last thread tick, + if (digitalRead(joystick[i].pin) != joystickActiveLogic) { + joystick[i].onUp(); // Run callback: press has ended (possible release of a hold) + joystick[i].state = State::REST; // Mark that the button has reset + } + // If button not yet released + else { + awaitingRelease = true; // Mark that polling-for-release should continue + if (length >= joystickDebounceLength) { + // Run callback: long press (once) + // Then continue waiting for release, to rearm + joystick[i].state = State::POLLING_FIRED; + joystick[i].onPress(); + } + } + break; + } + + // Button still held after press + // Just waiting for release + case POLLING_FIRED: + // Release detected + if (digitalRead(joystick[i].pin) != joystickActiveLogic) { + joystick[i].state = State::REST; + joystick[i].onUp(); // Callback: release of hold + } + // Not yet released, keep polling + else + awaitingRelease = true; + break; + } + } + + // If all buttons are now released + // we don't need to waste cpu resources polling + // IRQ will restart this thread when we next need it + if (!awaitingRelease) + stopThread(); + + // Run this method again, or don't.. + // Use whatever behavior was previously set by stopThread() or startThread() + return OSThread::interval; +} + +#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 TwoButtonExtended::beforeLightSleep(void *unused) +{ + stop(); + 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 TwoButtonExtended::afterLightSleep(esp_sleep_wakeup_cause_t cause) +{ + start(); + + // Manually trigger the button-down ISR + // - during light sleep, our ISR is disabled + // - if light sleep ends by button press, pretend our own ISR caught it + // - need to manually confirm by reading pin ourselves, to avoid occasional false positives + // (false positive only when using internal pullup resistors?) + if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic) + isrPrimary(); + + return 0; // Indicates success +} + +#endif + +#endif diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.h b/src/graphics/niche/Inputs/TwoButtonExtended.h new file mode 100644 index 000000000..23fd78a2a --- /dev/null +++ b/src/graphics/niche/Inputs/TwoButtonExtended.h @@ -0,0 +1,136 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +/* + +Re-usable NicheGraphics input source + +Short and Long press for up to two buttons +Interrupt driven + +*/ + +/* + +This expansion adds support for four more buttons +These buttons are single-action only, no long press +Interrupt driven + +*/ + +#pragma once + +#include "configuration.h" + +#include "assert.h" +#include "functional" + +#ifdef ARCH_ESP32 +#include "esp_sleep.h" // For light-sleep handling +#endif + +#include "Observer.h" + +namespace NicheGraphics::Inputs +{ + +class TwoButtonExtended : protected concurrency::OSThread +{ + public: + typedef std::function Callback; + + static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition + + static TwoButtonExtended *getInstance(); // Create or get the singleton instance + void start(); // Start handling button input + void stop(); // Stop handling button input (disconnect ISRs for sleep) + void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); + void setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup = false); + void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); + void setJoystickDebounce(uint32_t debounceMs); + void setHandlerDown(uint8_t whichButton, Callback onDown); + void setHandlerUp(uint8_t whichButton, Callback onUp); + void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); + void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); + void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown); + void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp); + void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress); + + // Disconnect and reconnect interrupts for light sleep +#ifdef ARCH_ESP32 + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); +#endif + + private: + // Internal state of a specific button + enum State { + REST, // Up, no activity + IRQ, // Down detected, not yet handled + POLLING_UNFIRED, // Down handled, polling for release + POLLING_FIRED, // Longpress fired, button still held + }; + + // Joystick Directions + enum Direction { UP = 0, DOWN, LEFT, RIGHT }; + + // Data used for direction (single-action) buttons + class SimpleButton + { + public: + // Per-button config + uint8_t pin = 0xFF; // 0xFF: unset + volatile State state = State::REST; // Internal state + volatile uint32_t irqAtMillis; // millis() when button went down + + // Per-button event callbacks + static void noop(){}; + std::function onDown = noop; + std::function onUp = noop; + std::function onPress = noop; + }; + + // Data used for double-action buttons + class Button : public SimpleButton + { + public: + // Per-button extended config + bool activeLogic = LOW; // Active LOW by default. + uint32_t debounceLength = 50; // Minimum length for shortpress in ms + uint32_t longpressLength = 500; // Time until longpress in ms + + // Per-button event callbacks + std::function onLongPress = noop; + }; + +#ifdef ARCH_ESP32 + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = + CallbackObserver(this, &TwoButtonExtended::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &TwoButtonExtended::afterLightSleep); +#endif + + int32_t runOnce() override; // Timer method. Polls for button release + + void startThread(); // Start polling for release + void stopThread(); // Stop polling for release + + static void isrPrimary(); // User Button ISR + static void isrSecondary(); // optional aux button or joystick center + static void isrJoystickUp(); + static void isrJoystickDown(); + static void isrJoystickLeft(); + static void isrJoystickRight(); + + TwoButtonExtended(); // Constructor made private: force use of Button::instance() + + // Info about both buttons + Button buttons[2]; + bool joystickActiveLogic = LOW; // Active LOW by default + uint32_t joystickDebounceLength = 50; // time until press in ms + SimpleButton joystick[4]; +}; + +}; // namespace NicheGraphics::Inputs + +#endif diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp index 9f53b06f4..0d835a3a9 100644 --- a/src/input/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -37,6 +37,9 @@ bool ButtonThread::initButton(const ButtonConfig &config) _activeLow = config.activeLow; _touchQuirk = config.touchQuirk; _intRoutine = config.intRoutine; + _pressHandler = config.onPress; + _releaseHandler = config.onRelease; + _suppressLeadUp = config.suppressLeadUpSound; _longLongPress = config.longLongPress; userButton = OneButton(config.pinNumber, config.activeLow, config.activePullup); @@ -133,6 +136,8 @@ int32_t ButtonThread::runOnce() // Detect start of button press if (buttonCurrentlyPressed && !buttonWasPressed) { + if (_pressHandler) + _pressHandler(); buttonPressStartTime = millis(); leadUpPlayed = false; leadUpSequenceActive = false; @@ -140,7 +145,7 @@ int32_t ButtonThread::runOnce() } // Progressive lead-up sound system - if (buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) { + if (!_suppressLeadUp && buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) { // Start the progressive sequence if not already active if (!leadUpSequenceActive) { @@ -160,6 +165,8 @@ int32_t ButtonThread::runOnce() // Reset when button is released if (!buttonCurrentlyPressed && buttonWasPressed) { + if (_releaseHandler) + _releaseHandler(); leadUpSequenceActive = false; resetLeadUpSequence(); } @@ -241,7 +248,21 @@ int32_t ButtonThread::runOnce() this->notifyObservers(&evt); playComboTune(); break; - +#if !HAS_SCREEN + case 4: + if (moduleConfig.external_notification.enabled && externalNotificationModule) { + externalNotificationModule->setMute(!externalNotificationModule->getMute()); + IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow();) + if (externalNotificationModule->getMute()) { + LOG_INFO("Temporarily Muted"); + play4ClickDown(); // Disable tone + } else { + LOG_INFO("Unmuted"); + play4ClickUp(); // Enable tone + } + } + break; +#endif // No valid multipress action default: break; diff --git a/src/input/ButtonThread.h b/src/input/ButtonThread.h index 7de38341c..e724c3596 100644 --- a/src/input/ButtonThread.h +++ b/src/input/ButtonThread.h @@ -13,6 +13,9 @@ struct ButtonConfig { bool activePullup = true; uint32_t pullupSense = 0; voidFuncPtr intRoutine = nullptr; + voidFuncPtr onPress = nullptr; // Optional edge callbacks + voidFuncPtr onRelease = nullptr; // Optional edge callbacks + bool suppressLeadUpSound = false; input_broker_event singlePress = INPUT_BROKER_NONE; input_broker_event longPress = INPUT_BROKER_NONE; uint16_t longPressTime = 500; @@ -94,6 +97,9 @@ class ButtonThread : public Observable, public concurrency:: input_broker_event _shortLong = INPUT_BROKER_NONE; voidFuncPtr _intRoutine = nullptr; + voidFuncPtr _pressHandler = nullptr; + voidFuncPtr _releaseHandler = nullptr; + bool _suppressLeadUp = false; uint16_t _longPressTime = 500; uint16_t _longLongPressTime = 3900; int _pinNum = 0; diff --git a/src/input/HackadayCommunicatorKeyboard.cpp b/src/input/HackadayCommunicatorKeyboard.cpp index 87c8a24ae..c6a9e0ae8 100644 --- a/src/input/HackadayCommunicatorKeyboard.cpp +++ b/src/input/HackadayCommunicatorKeyboard.cpp @@ -20,20 +20,20 @@ constexpr uint8_t modifierLeftShift = 0b0001; // Num chars per key, Modulus for rotating through characters static uint8_t HackadayCommunicatorTapMod[_TCA8418_NUM_KEYS] = { - 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 2, 2, 2, 1, 2, 2, 0, 0, 0, 2, 1, 2, 2, 0, 1, 1, 0, }; static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{}, - {}, + {Key::FUNCTION_F1}, {'+'}, {'9'}, {'8'}, {'7'}, - {'2'}, - {'3'}, - {'4'}, - {'5'}, + {Key::FUNCTION_F2}, + {Key::FUNCTION_F3}, + {Key::FUNCTION_F4}, + {Key::FUNCTION_F5}, {Key::ESC}, {'q', 'Q'}, {'w', 'W'}, @@ -141,6 +141,7 @@ void HackadayCommunicatorKeyboard::pressed(uint8_t key) if (state == Init || state == Busy) { return; } + LOG_DEBUG("Key pressed: %u", key); if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { modifierFlag = 0; diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index 0aa78e2b8..c0a56233f 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -1,8 +1,59 @@ #include "InputBroker.h" #include "PowerFSM.h" // needed for event trigger #include "configuration.h" +#include "graphics/Screen.h" #include "modules/ExternalNotificationModule.h" +#if ARCH_PORTDUINO +#include "input/LinuxInputImpl.h" +#include "input/SeesawRotary.h" +#include "platform/portduino/PortduinoGlue.h" +#endif + +#if !MESHTASTIC_EXCLUDE_INPUTBROKER +#include "input/ExpressLRSFiveWay.h" +#include "input/RotaryEncoderImpl.h" +#include "input/RotaryEncoderInterruptImpl1.h" +#include "input/SerialKeyboardImpl.h" +#include "input/UpDownInterruptImpl1.h" +#include "input/i2cButton.h" +#if HAS_TRACKBALL +#include "input/TrackballInterruptImpl1.h" +#endif + +#include "modules/StatusLEDModule.h" + +#if !MESHTASTIC_EXCLUDE_I2C +#include "input/cardKbI2cImpl.h" +#endif +#include "input/kbMatrixImpl.h" +#endif + +#if HAS_BUTTON || defined(ARCH_PORTDUINO) +#include "input/ButtonThread.h" + +#if defined(BUTTON_PIN_TOUCH) +ButtonThread *TouchButtonThread = nullptr; +#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) +static bool touchBacklightWasOn = false; +static bool touchBacklightActive = false; +#endif +#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 + InputBroker *inputBroker = nullptr; InputBroker::InputBroker() @@ -74,3 +125,262 @@ void InputBroker::pollSoonWorker(void *p) vTaskDelete(NULL); } #endif + +void InputBroker::Init() +{ + +#ifdef BUTTON_PIN +#ifdef ARCH_ESP32 + +#if ESP_ARDUINO_VERSION_MAJOR >= 3 +#ifdef BUTTON_NEED_PULLUP + pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP); +#else + pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN +#endif +#else + pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN +#ifdef BUTTON_NEED_PULLUP + 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 + +// 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 (portduino_config.userButtonPin.enabled) { + + LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin); + UserButtonThread = new ButtonThread("UserButton"); + if (screen) { + ButtonConfig config; + config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin; + config.activeLow = true; + config.activePullup = true; + config.pullupSense = INPUT_PULLUP; + config.intRoutine = []() { + UserButtonThread->userButton.tick(); + UserButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + config.singlePress = INPUT_BROKER_USER_PRESS; + config.longPress = INPUT_BROKER_SELECT; + UserButtonThread->initButton(config); + } + } +#endif + +#ifdef BUTTON_PIN_TOUCH + TouchButtonThread = new ButtonThread("BackButton"); + ButtonConfig touchConfig; + touchConfig.pinNumber = BUTTON_PIN_TOUCH; + touchConfig.activeLow = true; + touchConfig.activePullup = true; + touchConfig.pullupSense = pullup_sense; + touchConfig.intRoutine = []() { + TouchButtonThread->userButton.tick(); + TouchButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + touchConfig.singlePress = INPUT_BROKER_NONE; + touchConfig.longPress = INPUT_BROKER_BACK; +#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) + // On T-Echo Plus the touch pad should only drive the backlight, not UI navigation/sounds + touchConfig.longPress = INPUT_BROKER_NONE; + touchConfig.suppressLeadUpSound = true; + touchConfig.onPress = []() { + touchBacklightWasOn = uiconfig.screen_brightness == 1; + if (!touchBacklightWasOn) { + digitalWrite(PIN_EINK_EN, HIGH); + } + touchBacklightActive = true; + }; + touchConfig.onRelease = []() { + if (touchBacklightActive && !touchBacklightWasOn) { + digitalWrite(PIN_EINK_EN, LOW); + } + touchBacklightActive = false; + }; +#endif + TouchButtonThread->initButton(touchConfig); +#endif + +#if defined(CANCEL_BUTTON_PIN) + // Buttons. Moved here cause we need NodeDB to be initialized + CancelButtonThread = new ButtonThread("CancelButton"); + ButtonConfig cancelConfig; + cancelConfig.pinNumber = CANCEL_BUTTON_PIN; + cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW; + cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP; + cancelConfig.pullupSense = pullup_sense; + cancelConfig.intRoutine = []() { + CancelButtonThread->userButton.tick(); + CancelButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + cancelConfig.singlePress = INPUT_BROKER_CANCEL; + cancelConfig.longPress = INPUT_BROKER_SHUTDOWN; + cancelConfig.longPressTime = 4000; + CancelButtonThread->initButton(cancelConfig); +#endif + +#if defined(ALT_BUTTON_PIN) + // Buttons. Moved here cause we need NodeDB to be initialized + BackButtonThread = new ButtonThread("BackButton"); + ButtonConfig backConfig; + backConfig.pinNumber = ALT_BUTTON_PIN; + backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW; + backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP; + backConfig.pullupSense = pullup_sense; + backConfig.intRoutine = []() { + BackButtonThread->userButton.tick(); + BackButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + backConfig.singlePress = INPUT_BROKER_ALT_PRESS; + backConfig.longPress = INPUT_BROKER_ALT_LONG; + backConfig.longPressTime = 500; + BackButtonThread->initButton(backConfig); +#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) { + ButtonConfig userConfig; + userConfig.pinNumber = (uint8_t)_pinNum; + userConfig.activeLow = BUTTON_ACTIVE_LOW; + userConfig.activePullup = BUTTON_ACTIVE_PULLUP; + userConfig.pullupSense = pullup_sense; + userConfig.intRoutine = []() { + UserButtonThread->userButton.tick(); + UserButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + userConfig.singlePress = INPUT_BROKER_USER_PRESS; + userConfig.longPress = INPUT_BROKER_SELECT; + userConfig.longPressTime = 500; + userConfig.longLongPress = INPUT_BROKER_SHUTDOWN; + UserButtonThread->initButton(userConfig); + } else { + ButtonConfig userConfigNoScreen; + userConfigNoScreen.pinNumber = (uint8_t)_pinNum; + userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW; + userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP; + userConfigNoScreen.pullupSense = pullup_sense; + userConfigNoScreen.intRoutine = []() { + UserButtonThread->userButton.tick(); + UserButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS; + userConfigNoScreen.longPress = INPUT_BROKER_NONE; + userConfigNoScreen.longPressTime = 500; + userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN; + userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING; + userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE; + UserButtonThread->initButton(userConfigNoScreen); + } +#endif +#endif + +#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { +#if defined(T_LORA_PAGER) + // use a special FSM based rotary encoder version for T-LoRa Pager + rotaryEncoderImpl = new RotaryEncoderImpl(); + if (!rotaryEncoderImpl->init()) { + delete rotaryEncoderImpl; + rotaryEncoderImpl = nullptr; + } +#elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2) + upDownInterruptImpl1 = new UpDownInterruptImpl1(); + if (!upDownInterruptImpl1->init()) { + delete upDownInterruptImpl1; + upDownInterruptImpl1 = nullptr; + } +#else + rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); + if (!rotaryEncoderInterruptImpl1->init()) { + delete rotaryEncoderInterruptImpl1; + rotaryEncoderInterruptImpl1 = nullptr; + } +#endif + cardKbI2cImpl = new CardKbI2cImpl(); + cardKbI2cImpl->init(); +#if defined(M5STACK_UNITC6L) + i2cButton = new i2cButtonThread("i2cButtonThread"); +#endif +#ifdef INPUTBROKER_MATRIX_TYPE + kbMatrixImpl = new KbMatrixImpl(); + kbMatrixImpl->init(); +#endif // INPUTBROKER_MATRIX_TYPE +#ifdef INPUTBROKER_SERIAL_TYPE + aSerialKeyboardImpl = new SerialKeyboardImpl(); + aSerialKeyboardImpl->init(); +#endif // INPUTBROKER_MATRIX_TYPE + } +#endif // HAS_BUTTON +#if ARCH_PORTDUINO + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") { + seesawRotary = new SeesawRotary("SeesawRotary"); + if (!seesawRotary->init()) { + delete seesawRotary; + seesawRotary = nullptr; + } + aLinuxInputImpl = new LinuxInputImpl(); + aLinuxInputImpl->init(); + } +#endif +#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL + 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 +} diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index 022101f7d..9fcdd845f 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -1,6 +1,7 @@ #pragma once #include "Observer.h" +#include "concurrency/OSThread.h" #include "freertosinc.h" #ifdef InputBrokerDebug @@ -27,6 +28,11 @@ enum input_broker_event { INPUT_BROKER_SHUTDOWN = 0x9b, INPUT_BROKER_GPS_TOGGLE = 0x9e, INPUT_BROKER_SEND_PING = 0xaf, + INPUT_BROKER_FN_F1 = 0xf1, + INPUT_BROKER_FN_F2 = 0xf2, + INPUT_BROKER_FN_F3 = 0xf3, + INPUT_BROKER_FN_F4 = 0xf4, + INPUT_BROKER_FN_F5 = 0xf5, INPUT_BROKER_MATRIXKEY = 0xFE, INPUT_BROKER_ANYKEY = 0xff @@ -53,6 +59,7 @@ typedef struct _InputEvent { class InputPollable { public: + virtual ~InputPollable() = default; virtual void pollOnce() = 0; }; @@ -70,6 +77,7 @@ class InputBroker : public Observable void queueInputEvent(const InputEvent *event); void processInputEventQueue(); #endif + void Init(); protected: int handleInputEvent(const InputEvent *event); @@ -83,4 +91,5 @@ class InputBroker : public Observable #endif }; -extern InputBroker *inputBroker; \ No newline at end of file +extern InputBroker *inputBroker; +extern bool runASAP; \ No newline at end of file diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp index 7b43fa256..cc1222595 100644 --- a/src/input/RotaryEncoderImpl.cpp +++ b/src/input/RotaryEncoderImpl.cpp @@ -3,6 +3,9 @@ #include "RotaryEncoderImpl.h" #include "InputBroker.h" #include "RotaryEncoder.h" +#ifdef ARCH_ESP32 +#include "sleep.h" +#endif #define ORIGIN_NAME "RotaryEncoder" @@ -11,6 +14,20 @@ RotaryEncoderImpl *rotaryEncoderImpl; RotaryEncoderImpl::RotaryEncoderImpl() { rotary = nullptr; +#ifdef ARCH_ESP32 + isFirstInit = true; +#endif +} + +RotaryEncoderImpl::~RotaryEncoderImpl() +{ + LOG_DEBUG("RotaryEncoderImpl destructor"); + detachRotaryEncoderInterrupts(); + + if (rotary != nullptr) { + delete rotary; + rotary = nullptr; + } } bool RotaryEncoderImpl::init() @@ -25,15 +42,22 @@ bool RotaryEncoderImpl::init() eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); - rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, - moduleConfig.canned_message.inputbroker_pin_press); - rotary->resetButton(); + if (rotary == nullptr) { + rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, + moduleConfig.canned_message.inputbroker_pin_press); + } - interruptInstance = this; - auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); }; - attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE); - attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE); - attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE); + attachRotaryEncoderInterrupts(); + +#ifdef ARCH_ESP32 + // Register callbacks for before and after lightsleep + // Used to detach and reattach interrupts + if (isFirstInit) { + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); + isFirstInit = false; + } +#endif LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw, @@ -71,6 +95,50 @@ void RotaryEncoderImpl::pollOnce() } } +void RotaryEncoderImpl::detachRotaryEncoderInterrupts() +{ + LOG_DEBUG("RotaryEncoderImpl detach button interrupts"); + if (interruptInstance == this) { + detachInterrupt(moduleConfig.canned_message.inputbroker_pin_a); + detachInterrupt(moduleConfig.canned_message.inputbroker_pin_b); + detachInterrupt(moduleConfig.canned_message.inputbroker_pin_press); + interruptInstance = nullptr; + } else { + LOG_WARN("RotaryEncoderImpl: interrupts already detached"); + } +} + +void RotaryEncoderImpl::attachRotaryEncoderInterrupts() +{ + LOG_DEBUG("RotaryEncoderImpl attach button interrupts"); + if (rotary != nullptr && interruptInstance == nullptr) { + rotary->resetButton(); + + interruptInstance = this; + auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); }; + attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE); + attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE); + attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE); + } else { + LOG_WARN("RotaryEncoderImpl: interrupts already attached"); + } +} + +#ifdef ARCH_ESP32 + +int RotaryEncoderImpl::beforeLightSleep(void *unused) +{ + detachRotaryEncoderInterrupts(); + return 0; // Indicates success; +} + +int RotaryEncoderImpl::afterLightSleep(esp_sleep_wakeup_cause_t cause) +{ + attachRotaryEncoderInterrupts(); + return 0; // Indicates success; +} +#endif + RotaryEncoderImpl *RotaryEncoderImpl::interruptInstance; #endif \ No newline at end of file diff --git a/src/input/RotaryEncoderImpl.h b/src/input/RotaryEncoderImpl.h index 6f8e9fe5f..ec8a064bd 100644 --- a/src/input/RotaryEncoderImpl.h +++ b/src/input/RotaryEncoderImpl.h @@ -8,12 +8,18 @@ class RotaryEncoder; -class RotaryEncoderImpl : public InputPollable +class RotaryEncoderImpl final : public InputPollable { public: RotaryEncoderImpl(); - bool init(void); + ~RotaryEncoderImpl() override; + bool init(); virtual void pollOnce() override; + // Disconnect and reconnect interrupts for light sleep +#ifdef ARCH_ESP32 + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); +#endif protected: static RotaryEncoderImpl *interruptInstance; @@ -23,6 +29,21 @@ class RotaryEncoderImpl : public InputPollable input_broker_event eventPressed = INPUT_BROKER_NONE; RotaryEncoder *rotary; + + private: +#ifdef ARCH_ESP32 + bool isFirstInit; +#endif + void detachRotaryEncoderInterrupts(); + void attachRotaryEncoderInterrupts(); + +#ifdef ARCH_ESP32 + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = + CallbackObserver(this, &RotaryEncoderImpl::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &RotaryEncoderImpl::afterLightSleep); +#endif }; extern RotaryEncoderImpl *rotaryEncoderImpl; diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index c315f23d9..80ac08175 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -93,6 +93,8 @@ int32_t RotaryEncoderInterruptBase::runOnce() if (!pressDetected) { this->action = ROTARY_ACTION_NONE; + } else if (now - pressStartTime < LONG_PRESS_DURATION) { + return (20); // keep checking for long/short until time expires } return INT32_MAX; diff --git a/src/input/RotaryEncoderInterruptImpl1.cpp b/src/input/RotaryEncoderInterruptImpl1.cpp index 12cbc36fb..1da2ea008 100644 --- a/src/input/RotaryEncoderInterruptImpl1.cpp +++ b/src/input/RotaryEncoderInterruptImpl1.cpp @@ -27,7 +27,9 @@ bool RotaryEncoderInterruptImpl1::init() RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB, RotaryEncoderInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); +#ifndef HAS_PHYSICAL_KEYBOARD osk_found = true; +#endif return true; } diff --git a/src/input/SerialKeyboard.cpp b/src/input/SerialKeyboard.cpp index a5d2c614f..8037b0d57 100644 --- a/src/input/SerialKeyboard.cpp +++ b/src/input/SerialKeyboard.cpp @@ -5,7 +5,6 @@ SerialKeyboard *globalSerialKeyboard = nullptr; #ifdef INPUTBROKER_SERIAL_TYPE -#define CANNED_MESSAGE_MODULE_ENABLE 1 // in case it's not set in the variant file #if INPUTBROKER_SERIAL_TYPE == 1 // It's a Chatter // 3 SHIFT level (lower case, upper case, numbers), up to 4 repeated presses, button number diff --git a/src/input/TCA8418KeyboardBase.h b/src/input/TCA8418KeyboardBase.h index 8e509ac7e..e608c6da5 100644 --- a/src/input/TCA8418KeyboardBase.h +++ b/src/input/TCA8418KeyboardBase.h @@ -26,7 +26,12 @@ class TCA8418KeyboardBase GPS_TOGGLE = 0x9E, MUTE_TOGGLE = 0xAC, SEND_PING = 0xAF, - BL_TOGGLE = 0xAB + BL_TOGGLE = 0xAB, + FUNCTION_F1 = 0xF1, + FUNCTION_F2 = 0xF2, + FUNCTION_F3 = 0xF3, + FUNCTION_F4 = 0xF4, + FUNCTION_F5 = 0xF5 }; typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index d2025c192..1bbe75629 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -1,5 +1,7 @@ #include "TrackballInterruptBase.h" +#include "Throttle.h" #include "configuration.h" + extern bool osk_found; TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {} @@ -45,7 +47,9 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, pinPress); +#ifndef HAS_PHYSICAL_KEYBOARD osk_found = true; +#endif this->setInterval(100); } @@ -53,17 +57,27 @@ int32_t TrackballInterruptBase::runOnce() { InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; +#if TB_THRESHOLD + if (lastInterruptTime && !Throttle::isWithinTimespanMs(lastInterruptTime, 1000)) { + left_counter = 0; + right_counter = 0; + up_counter = 0; + down_counter = 0; + lastInterruptTime = 0; + } +#ifdef INPUT_DEBUG + if (left_counter > 0 || right_counter > 0 || up_counter > 0 || down_counter > 0) { + LOG_DEBUG("L %u R %u U %u D %u, time %u", left_counter, right_counter, up_counter, down_counter, millis()); + } +#endif +#endif // Handle long press detection for press button if (pressDetected && pressStartTime > 0) { uint32_t pressDuration = millis() - pressStartTime; bool buttonStillPressed = false; -#if defined(T_DECK) - buttonStillPressed = (this->action == TB_ACTION_PRESSED); -#else buttonStillPressed = !digitalRead(_pinPress); -#endif if (!buttonStillPressed) { // Button released @@ -132,23 +146,31 @@ int32_t TrackballInterruptBase::runOnce() } } -#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball - if (this->action == TB_ACTION_PRESSED && !pressDetected) { +#if TB_THRESHOLD + if (this->action == TB_ACTION_PRESSED && (!pressDetected || pressStartTime == 0)) { // Start long press detection pressDetected = true; pressStartTime = millis(); // Don't send event yet, wait to see if it's a long press - } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { - // LOG_DEBUG("Trackball event UP"); + } else if (up_counter >= TB_THRESHOLD) { +#ifdef INPUT_DEBUG + LOG_DEBUG("Trackball event UP %u", millis()); +#endif e.inputEvent = this->_eventUp; - } else if (this->action == TB_ACTION_DOWN && lastEvent == TB_ACTION_DOWN) { - // LOG_DEBUG("Trackball event DOWN"); + } else if (down_counter >= TB_THRESHOLD) { +#ifdef INPUT_DEBUG + LOG_DEBUG("Trackball event DOWN %u", millis()); +#endif e.inputEvent = this->_eventDown; - } else if (this->action == TB_ACTION_LEFT && lastEvent == TB_ACTION_LEFT) { - // LOG_DEBUG("Trackball event LEFT"); + } else if (left_counter >= TB_THRESHOLD) { +#ifdef INPUT_DEBUG + LOG_DEBUG("Trackball event LEFT %u", millis()); +#endif e.inputEvent = this->_eventLeft; - } else if (this->action == TB_ACTION_RIGHT && lastEvent == TB_ACTION_RIGHT) { - // LOG_DEBUG("Trackball event RIGHT"); + } else if (right_counter >= TB_THRESHOLD) { +#ifdef INPUT_DEBUG + LOG_DEBUG("Trackball event RIGHT %u", millis()); +#endif e.inputEvent = this->_eventRight; } #else @@ -181,6 +203,12 @@ int32_t TrackballInterruptBase::runOnce() e.source = this->_originName; e.kbchar = 0x00; this->notifyObservers(&e); +#if TB_THRESHOLD + left_counter = 0; + right_counter = 0; + up_counter = 0; + down_counter = 0; +#endif } // Only update lastEvent for non-press actions or completed press actions @@ -196,25 +224,49 @@ int32_t TrackballInterruptBase::runOnce() void TrackballInterruptBase::intPressHandler() { - this->action = TB_ACTION_PRESSED; + if (!Throttle::isWithinTimespanMs(lastInterruptTime, 10)) + this->action = TB_ACTION_PRESSED; + lastInterruptTime = millis(); } void TrackballInterruptBase::intDownHandler() { - this->action = TB_ACTION_DOWN; + if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) + this->action = TB_ACTION_DOWN; + lastInterruptTime = millis(); + +#if TB_THRESHOLD + down_counter++; +#endif } void TrackballInterruptBase::intUpHandler() { - this->action = TB_ACTION_UP; + if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) + this->action = TB_ACTION_UP; + lastInterruptTime = millis(); + +#if TB_THRESHOLD + up_counter++; +#endif } void TrackballInterruptBase::intLeftHandler() { - this->action = TB_ACTION_LEFT; + if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) + this->action = TB_ACTION_LEFT; + lastInterruptTime = millis(); +#if TB_THRESHOLD + left_counter++; +#endif } void TrackballInterruptBase::intRightHandler() { - this->action = TB_ACTION_RIGHT; + if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) + this->action = TB_ACTION_RIGHT; + lastInterruptTime = millis(); +#if TB_THRESHOLD + right_counter++; +#endif } diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index 67d4ee449..908f62769 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -12,6 +12,10 @@ #endif #endif +#ifndef TB_THRESHOLD +#define TB_THRESHOLD 0 +#endif + class TrackballInterruptBase : public Observable, public concurrency::OSThread { public: @@ -25,8 +29,6 @@ class TrackballInterruptBase : public Observable, public con void intUpHandler(); void intLeftHandler(); void intRightHandler(); - uint32_t lastTime = 0; - virtual int32_t runOnce() override; protected: @@ -67,4 +69,12 @@ class TrackballInterruptBase : public Observable, public con input_broker_event _eventPressedLong = INPUT_BROKER_NONE; const char *_originName; TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; + volatile uint32_t lastInterruptTime = 0; + +#if TB_THRESHOLD + volatile uint8_t left_counter = 0; + volatile uint8_t right_counter = 0; + volatile uint8_t up_counter = 0; + volatile uint8_t down_counter = 0; +#endif }; diff --git a/src/input/TrackballInterruptImpl1.cpp b/src/input/TrackballInterruptImpl1.cpp index 594facdeb..fd126913a 100644 --- a/src/input/TrackballInterruptImpl1.cpp +++ b/src/input/TrackballInterruptImpl1.cpp @@ -24,41 +24,26 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe void TrackballInterruptImpl1::handleIntDown() { - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intDownHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } + trackballInterruptImpl1->intDownHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); } void TrackballInterruptImpl1::handleIntUp() { - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intUpHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } + trackballInterruptImpl1->intUpHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); } void TrackballInterruptImpl1::handleIntLeft() { - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intLeftHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } + trackballInterruptImpl1->intLeftHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); } void TrackballInterruptImpl1::handleIntRight() { - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intRightHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } + trackballInterruptImpl1->intRightHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); } void TrackballInterruptImpl1::handleIntPressed() { - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intPressHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } + trackballInterruptImpl1->intPressHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); } diff --git a/src/input/UpDownInterruptImpl1.cpp b/src/input/UpDownInterruptImpl1.cpp index 9b0b1f39e..906dcd2a8 100644 --- a/src/input/UpDownInterruptImpl1.cpp +++ b/src/input/UpDownInterruptImpl1.cpp @@ -29,7 +29,9 @@ bool UpDownInterruptImpl1::init() eventDownLong, UpDownInterruptImpl1::handleIntDown, UpDownInterruptImpl1::handleIntUp, UpDownInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); +#ifndef HAS_PHYSICAL_KEYBOARD osk_found = true; +#endif return true; } diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index 0085c806b..d744ee2ca 100644 --- a/src/input/kbI2cBase.cpp +++ b/src/input/kbI2cBase.cpp @@ -321,6 +321,26 @@ int32_t KbI2cBase::runOnce() e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_TAB; break; + case TCA8418KeyboardBase::FUNCTION_F1: + e.inputEvent = INPUT_BROKER_FN_F1; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::FUNCTION_F2: + e.inputEvent = INPUT_BROKER_FN_F2; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::FUNCTION_F3: + e.inputEvent = INPUT_BROKER_FN_F3; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::FUNCTION_F4: + e.inputEvent = INPUT_BROKER_FN_F4; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::FUNCTION_F5: + e.inputEvent = INPUT_BROKER_FN_F5; + e.kbchar = 0x00; + break; default: if (nextEvent > 127) { e.inputEvent = INPUT_BROKER_NONE; @@ -489,8 +509,6 @@ int32_t KbI2cBase::runOnce() case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT case 0x91: // fn+t case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE - - 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 diff --git a/src/main.cpp b/src/main.cpp index f8d89e1ba..1e0ec041e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ #include "ReliableRouter.h" #include "airtime.h" #include "buzz.h" +#include "power/PowerHAL.h" #include "FSCommon.h" #include "Led.h" @@ -38,9 +39,8 @@ #include "target_specific.h" #include #include - -#ifdef ELECROW_ThinkNode_M5 -PCA9557 io(0x18, &Wire); +#if HAS_SCREEN +#include "MessageStore.h" #endif #ifdef ARCH_ESP32 @@ -73,55 +73,52 @@ NRF52Bluetooth *nrf52Bluetooth = nullptr; #include "mqtt/MQTT.h" #endif -#include "LLCC68Interface.h" -#include "LR1110Interface.h" -#include "LR1120Interface.h" -#include "LR1121Interface.h" -#include "RF95Interface.h" -#include "SX1262Interface.h" -#include "SX1268Interface.h" -#include "SX1280Interface.h" -#include "detect/LoRaRadioType.h" - -#ifdef ARCH_STM32WL -#include "STM32WLE5JCInterface.h" -#endif - -#if defined(ARCH_PORTDUINO) -#include "platform/portduino/SimRadio.h" -#endif - #ifdef ARCH_PORTDUINO #include "linux/LinuxHardwareI2C.h" #include "mesh/raspihttp/PiWebServer.h" #include "platform/portduino/PortduinoGlue.h" -#include "platform/portduino/USBHal.h" #include #include #include #include #endif -#if HAS_BUTTON || defined(ARCH_PORTDUINO) -#include "input/ButtonThread.h" +#ifdef ARCH_ESP32 +#ifdef DEBUG_PARTITION_TABLE +#include "esp_partition.h" -#if defined(BUTTON_PIN_TOUCH) -ButtonThread *TouchButtonThread = nullptr; -#endif +void printPartitionTable() +{ + printf("\n--- Partition Table ---\n"); + // Print Column Headers + printf("| %-16s | %-4s | %-7s | %-10s | %-10s |\n", "Label", "Type", "Subtype", "Offset", "Size"); + printf("|------------------|------|---------|------------|------------|\n"); -#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) -ButtonThread *UserButtonThread = nullptr; -#endif + // Create an iterator to find ALL partitions (Type ANY, Subtype ANY) + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL); -#if defined(ALT_BUTTON_PIN) -ButtonThread *BackButtonThread = nullptr; -#endif + // Loop through the iterator + if (it != NULL) { + do { + const esp_partition_t *part = esp_partition_get(it); -#if defined(CANCEL_BUTTON_PIN) -ButtonThread *CancelButtonThread = nullptr; -#endif + // Print details: Label, Type (Hex), Subtype (Hex), Offset (Hex), Size (Hex) + printf("| %-16s | 0x%02x | 0x%02x | 0x%08x | 0x%08x |\n", part->label, part->type, part->subtype, part->address, + part->size); -#endif + // Move to next partition + it = esp_partition_next(it); + } while (it != NULL); + + // Release the iterator memory + esp_partition_iterator_release(it); + } else { + printf("No partitions found.\n"); + } + printf("-----------------------\n"); +} +#endif // DEBUG_PARTITION_TABLE +#endif // ARCH_ESP32 #include "AmbientLightingThread.h" #include "PowerFSMThread.h" @@ -205,13 +202,10 @@ ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, /// The I2C address of our Air Quality Indicator (if found) ScanI2C::DeviceAddress aqi_found = ScanI2C::ADDRESS_NONE; -#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) +#ifdef HAS_DRV2605 Adafruit_DRV2605 drv; #endif -// Global LoRa radio type -LoRaRadioType radioType = NO_RADIO; - bool isVibrating = false; bool eink_found = true; @@ -248,6 +242,7 @@ const char *getDeviceName() return name; } +// TODO remove from main.cpp static int32_t ledBlinker() { // Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if @@ -288,6 +283,46 @@ __attribute__((weak, noinline)) bool loopCanSleep() void lateInitVariant() __attribute__((weak)); void lateInitVariant() {} +void earlyInitVariant() __attribute__((weak)); +void earlyInitVariant() {} + +// NRF52 (and probably other platforms) can report when system is in power failure mode +// (eg. too low battery voltage) and operating it is unsafe (data corruption, bootloops, etc). +// For example NRF52 will prevent any flash writes in that case automatically +// (but it causes issues we need to handle). +// This detection is independent from whatever ADC or dividers used in Meshtastic +// boards and is internal to chip. + +// we use powerHAL layer to get this info and delay booting until power level is safe + +// wait until power level is safe to continue booting (to avoid bootloops) +// blink user led in 3 flashes sequence to indicate what is happening +void waitUntilPowerLevelSafe() +{ + +#ifdef LED_PIN + pinMode(LED_PIN, OUTPUT); +#endif + + while (powerHAL_isPowerLevelSafe() == false) { + +#ifdef LED_PIN + + // 3x: blink for 300 ms, pause for 300 ms + + for (int i = 0; i < 3; i++) { + digitalWrite(LED_PIN, LED_STATE_ON); + delay(300); + digitalWrite(LED_PIN, LED_STATE_OFF); + delay(300); + } +#endif + + // sleep for 2s + delay(2000); + } +} + /** * Print info as a structured log message (for automated log processing) */ @@ -298,34 +333,30 @@ void printInfo() #ifndef PIO_UNIT_TESTING void setup() { -#if defined(R1_NEO) - pinMode(DCDC_EN_HOLD, OUTPUT); - digitalWrite(DCDC_EN_HOLD, HIGH); - pinMode(NRF_ON, OUTPUT); - digitalWrite(NRF_ON, HIGH); -#endif + + // initialize power HAL layer as early as possible + powerHAL_init(); + + // prevent booting if device is in power failure mode + // boot sequence will follow when battery level raises to safe mode + waitUntilPowerLevelSafe(); + + // Defined in variant.cpp for early init code + earlyInitVariant(); #if defined(PIN_POWER_EN) pinMode(PIN_POWER_EN, OUTPUT); digitalWrite(PIN_POWER_EN, HIGH); #endif -#if defined(ELECROW_ThinkNode_M5) - Wire.begin(48, 47); - io.pinMode(PCA_PIN_EINK_EN, OUTPUT); - io.pinMode(PCA_PIN_POWER_EN, OUTPUT); - io.digitalWrite(PCA_PIN_POWER_EN, HIGH); - // io.pinMode(C2_PIN, OUTPUT); -#endif - #ifdef LED_POWER pinMode(LED_POWER, OUTPUT); digitalWrite(LED_POWER, LED_STATE_ON); #endif -#ifdef USER_LED - pinMode(USER_LED, OUTPUT); - digitalWrite(USER_LED, HIGH ^ LED_STATE_ON); +#ifdef LED_NOTIFICATION + pinMode(LED_NOTIFICATION, OUTPUT); + digitalWrite(LED_NOTIFICATION, HIGH ^ LED_STATE_ON); #endif #ifdef WIFI_LED @@ -335,75 +366,10 @@ void setup() #ifdef BLE_LED pinMode(BLE_LED, OUTPUT); -#ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); -#else - digitalWrite(BLE_LED, LOW); -#endif -#endif - -#if defined(T_DECK) - // GPIO10 manages all peripheral power supplies - // Turn on peripheral power immediately after MUC starts. - // If some boards are turned on late, ESP32 will reset due to low voltage. - // ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) , - // TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder) - pinMode(KB_POWERON, OUTPUT); - digitalWrite(KB_POWERON, HIGH); - // T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus - // We need to initialize all CS pins in advance otherwise there will be SPI communication issues - // e.g. when detecting the SD card - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(TFT_CS, OUTPUT); - digitalWrite(TFT_CS, HIGH); - delay(100); -#elif defined(T_DECK_PRO) - pinMode(LORA_EN, OUTPUT); - digitalWrite(LORA_EN, HIGH); - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(PIN_EINK_CS, OUTPUT); - digitalWrite(PIN_EINK_CS, HIGH); -#elif defined(T_LORA_PAGER) - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(TFT_CS, OUTPUT); - digitalWrite(TFT_CS, HIGH); - pinMode(KB_INT, INPUT_PULLUP); - // io expander - io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL); - io.pinMode(EXPANDS_DRV_EN, OUTPUT); - io.digitalWrite(EXPANDS_DRV_EN, HIGH); - io.pinMode(EXPANDS_AMP_EN, OUTPUT); - io.digitalWrite(EXPANDS_AMP_EN, LOW); - io.pinMode(EXPANDS_LORA_EN, OUTPUT); - io.digitalWrite(EXPANDS_LORA_EN, HIGH); - io.pinMode(EXPANDS_GPS_EN, OUTPUT); - io.digitalWrite(EXPANDS_GPS_EN, HIGH); - io.pinMode(EXPANDS_KB_EN, OUTPUT); - io.digitalWrite(EXPANDS_KB_EN, HIGH); - io.pinMode(EXPANDS_SD_EN, OUTPUT); - io.digitalWrite(EXPANDS_SD_EN, HIGH); - io.pinMode(EXPANDS_GPIO_EN, OUTPUT); - io.digitalWrite(EXPANDS_GPIO_EN, HIGH); - io.pinMode(EXPANDS_SD_PULLEN, INPUT); -#elif defined(HACKADAY_COMMUNICATOR) - pinMode(KB_INT, INPUT); + digitalWrite(BLE_LED, LED_STATE_OFF); #endif concurrency::hasBeenSetup = true; -#if ARCH_PORTDUINO - SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); -#else - SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); -#endif meshtastic_Config_DisplayConfig_OledType screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; @@ -428,10 +394,17 @@ void setup() #endif #if ARCH_PORTDUINO + RTCQuality ourQuality = RTCQualityDevice; + + std::string timeCommandResult = exec("timedatectl status | grep synchronized | grep yes -c"); + if (timeCommandResult[0] == '1') { + ourQuality = RTCQualityNTP; + } + struct timeval tv; tv.tv_sec = time(NULL); tv.tv_usec = 0; - perhapsSetRTC(RTCQualityDevice, &tv); + perhapsSetRTC(ourQuality, &tv); #endif powerMonInit(); @@ -439,6 +412,13 @@ void setup() LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n"); +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) +#ifndef SENSECAP_INDICATOR + // use PSRAM for malloc calls > 256 bytes + heap_caps_malloc_extmem_enable(256); +#endif +#endif + #if defined(DEBUG_MUTE) && defined(DEBUG_PORT) DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n"); DEBUG_PORT.printf("Version %s for %s from %s\r\n", optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO)); @@ -500,39 +480,18 @@ void setup() LOG_INFO("Wait for peripherals to stabilize"); delay(PERIPHERAL_WARMUP_MS); #endif - -#ifdef BUTTON_PIN -#ifdef ARCH_ESP32 - -#if ESP_ARDUINO_VERSION_MAJOR >= 3 -#ifdef BUTTON_NEED_PULLUP - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP); -#else - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN -#endif -#else - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN -#ifdef BUTTON_NEED_PULLUP - 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 - initSPI(); OSThread::setup(); + // TODO make this ifdef based on defined pins and move from main.cpp #if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2) // The ThinkNodes have their own blink logic // ledPeriodic = new Periodic("Blink", elecrowLedBlinker); #else + ledPeriodic = new Periodic("Blink", ledBlinker); + #endif fsInit(); @@ -553,6 +512,7 @@ void setup() Wire.setSCL(I2C_SCL); Wire.begin(); #elif defined(I2C_SDA) && !defined(ARCH_RP2040) + LOG_INFO("Starting Bus with (SDA) %d and (SCL) %d: ", I2C_SDA, I2C_SCL); Wire.begin(I2C_SDA, I2C_SCL); #elif defined(ARCH_PORTDUINO) if (portduino_config.i2cdev != "") { @@ -626,7 +586,11 @@ void setup() sensor_detected = true; #endif } - +#ifdef ARCH_ESP32 +#ifdef DEBUG_PARTITION_TABLE + printPartitionTable(); +#endif +#endif // ARCH_ESP32 #ifdef ARCH_ESP32 // Don't init display if we don't have one or we are waking headless due to a timer event if (wakeCause == ESP_SLEEP_WAKEUP_TIMER) { @@ -737,11 +701,12 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA219, meshtastic_TelemetrySensorType_INA219); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX17048, meshtastic_TelemetrySensorType_MAX17048); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310, meshtastic_TelemetrySensorType_QMC6310); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310U, meshtastic_TelemetrySensorType_QMC6310); + // TODO: Types need to be added meshtastic_TelemetrySensorType_QMC6310N + // scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310N, meshtastic_TelemetrySensorType_QMC6310N); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PMSA0031, meshtastic_TelemetrySensorType_PMSA003I); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102); @@ -781,7 +746,6 @@ void setup() // We do this as early as possible because this loads preferences from flash // 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(); @@ -827,7 +791,12 @@ void setup() #endif #endif -#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) +#ifdef HAS_DRV2605 +#if defined(PIN_DRV_EN) + pinMode(PIN_DRV_EN, OUTPUT); + digitalWrite(PIN_DRV_EN, HIGH); + delay(10); +#endif drv.begin(); drv.selectLibrary(1); // I2C trigger by sending 'go' command @@ -895,6 +864,7 @@ void setup() } #endif // HAS_SCREEN + // TODO Remove magic string // setup TZ prior to time actions. #if !MESHTASTIC_EXCLUDE_TZ LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string @@ -978,162 +948,9 @@ void setup() nodeDB->hasWarned = true; } #endif - -// 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 (portduino_config.userButtonPin.enabled) { - - LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin); - UserButtonThread = new ButtonThread("UserButton"); - if (screen) { - ButtonConfig config; - config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin; - config.activeLow = true; - config.activePullup = true; - config.pullupSense = INPUT_PULLUP; - config.intRoutine = []() { - UserButtonThread->userButton.tick(); - UserButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - config.singlePress = INPUT_BROKER_USER_PRESS; - config.longPress = INPUT_BROKER_SELECT; - UserButtonThread->initButton(config); - } - } -#endif - -#ifdef BUTTON_PIN_TOUCH - TouchButtonThread = new ButtonThread("BackButton"); - ButtonConfig touchConfig; - touchConfig.pinNumber = BUTTON_PIN_TOUCH; - touchConfig.activeLow = true; - touchConfig.activePullup = true; - touchConfig.pullupSense = pullup_sense; - touchConfig.intRoutine = []() { - TouchButtonThread->userButton.tick(); - TouchButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - touchConfig.singlePress = INPUT_BROKER_NONE; - touchConfig.longPress = INPUT_BROKER_BACK; - TouchButtonThread->initButton(touchConfig); -#endif - -#if defined(CANCEL_BUTTON_PIN) - // Buttons. Moved here cause we need NodeDB to be initialized - CancelButtonThread = new ButtonThread("CancelButton"); - ButtonConfig cancelConfig; - cancelConfig.pinNumber = CANCEL_BUTTON_PIN; - cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW; - cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP; - cancelConfig.pullupSense = pullup_sense; - cancelConfig.intRoutine = []() { - CancelButtonThread->userButton.tick(); - CancelButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - cancelConfig.singlePress = INPUT_BROKER_CANCEL; - cancelConfig.longPress = INPUT_BROKER_SHUTDOWN; - cancelConfig.longPressTime = 4000; - CancelButtonThread->initButton(cancelConfig); -#endif - -#if defined(ALT_BUTTON_PIN) - // Buttons. Moved here cause we need NodeDB to be initialized - BackButtonThread = new ButtonThread("BackButton"); - ButtonConfig backConfig; - backConfig.pinNumber = ALT_BUTTON_PIN; - backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW; - backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP; - backConfig.pullupSense = pullup_sense; - backConfig.intRoutine = []() { - BackButtonThread->userButton.tick(); - BackButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - backConfig.singlePress = INPUT_BROKER_ALT_PRESS; - backConfig.longPress = INPUT_BROKER_ALT_LONG; - backConfig.longPressTime = 500; - BackButtonThread->initButton(backConfig); -#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) { - ButtonConfig userConfig; - userConfig.pinNumber = (uint8_t)_pinNum; - userConfig.activeLow = BUTTON_ACTIVE_LOW; - userConfig.activePullup = BUTTON_ACTIVE_PULLUP; - userConfig.pullupSense = pullup_sense; - userConfig.intRoutine = []() { - UserButtonThread->userButton.tick(); - UserButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - userConfig.singlePress = INPUT_BROKER_USER_PRESS; - userConfig.longPress = INPUT_BROKER_SELECT; - userConfig.longPressTime = 500; - userConfig.longLongPress = INPUT_BROKER_SHUTDOWN; - UserButtonThread->initButton(userConfig); - } else { - ButtonConfig userConfigNoScreen; - userConfigNoScreen.pinNumber = (uint8_t)_pinNum; - userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW; - userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP; - userConfigNoScreen.pullupSense = pullup_sense; - userConfigNoScreen.intRoutine = []() { - UserButtonThread->userButton.tick(); - UserButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS; - userConfigNoScreen.longPress = INPUT_BROKER_NONE; - userConfigNoScreen.longPressTime = 500; - userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN; - userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING; - userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE; - UserButtonThread->initButton(userConfigNoScreen); - } -#endif - +#if !MESHTASTIC_EXCLUDE_INPUTBROKER + if (inputBroker) + inputBroker->Init(); #endif #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS @@ -1172,257 +989,7 @@ void setup() #endif #endif -#ifdef PIN_PWR_DELAY_MS - // This may be required to give the peripherals time to power up. - delay(PIN_PWR_DELAY_MS); -#endif - -#ifdef ARCH_PORTDUINO - // as one can't use a function pointer to the class constructor: - auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy) { - switch (portduino_config.lora_module) { - case use_rf95: - return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy); - case use_sx1262: - return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy); - case use_sx1268: - return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy); - case use_sx1280: - return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy); - case use_lr1110: - return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy); - case use_lr1120: - return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy); - case use_lr1121: - return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy); - case use_llcc68: - return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy); - case use_simradio: - return (RadioInterface *)new SimRadio; - default: - assert(0); // shouldn't happen - return (RadioInterface *)nullptr; - } - }; - - LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(), - portduino_config.lora_spi_dev.c_str()); - if (portduino_config.lora_spi_dev == "ch341") { - RadioLibHAL = ch341Hal; - } else { - RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); - } - rIf = - loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin, - portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin); - - if (!rIf->init()) { - LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str()); - delete rIf; - rIf = NULL; - exit(EXIT_FAILURE); - } else { - LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str()); - } - -#elif defined(HW_SPI1_DEVICE) - LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings); -#else // HW_SPI1_DEVICE - LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); -#endif - - // radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init) -#if defined(USE_STM32WLx) - if (!rIf) { - rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No STM32WL radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("STM32WL init success"); - radioType = STM32WLx_RADIO; - } - } -#endif - -#if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1 - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1); - if (!rIf->init()) { - LOG_WARN("No RF95 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("RF95 init success"); - radioType = RF95_RADIO; - } - } -#endif - -#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1 - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); -#ifdef SX126X_DIO3_TCXO_VOLTAGE - sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); -#endif - if (!sxIf->init()) { - LOG_WARN("No SX1262 radio"); - delete sxIf; - rIf = NULL; - } else { - LOG_INFO("SX1262 init success"); - rIf = sxIf; - radioType = SX1262_RADIO; - } - } -#endif - -#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL) - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - // try using the specified TCXO voltage - auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); - if (!sxIf->init()) { - LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - delete sxIf; - rIf = NULL; - } else { - LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - rIf = sxIf; - radioType = SX1262_RADIO; - } - } - - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - // If specified TCXO voltage fails, attempt to use DIO3 as a reference instead - rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("SX1262 init success, XTAL, Vref 0.0V"); - radioType = SX1262_RADIO; - } - } -#endif - -#if defined(USE_SX1268) -#if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL) - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - // try using the specified TCXO voltage - auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); - if (!sxIf->init()) { - LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - delete sxIf; - rIf = NULL; - } else { - LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - rIf = sxIf; - radioType = SX1268_RADIO; - } - } -#endif - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No SX1268 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("SX1268 init success"); - radioType = SX1268_RADIO; - } - } -#endif - -#if defined(USE_LLCC68) - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No LLCC68 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LLCC68 init success"); - radioType = LLCC68_RADIO; - } - } -#endif - -#if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1 - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN); - if (!rIf->init()) { - LOG_WARN("No LR1110 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LR1110 init success"); - radioType = LR1110_RADIO; - } - } -#endif - -#if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1 - if (!rIf) { - rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN); - if (!rIf->init()) { - LOG_WARN("No LR1120 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LR1120 init success"); - radioType = LR1120_RADIO; - } - } -#endif - -#if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1 - if (!rIf) { - rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN); - if (!rIf->init()) { - LOG_WARN("No LR1121 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LR1121 init success"); - radioType = LR1121_RADIO; - } - } -#endif - -#if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1 - if (!rIf) { - rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY); - if (!rIf->init()) { - LOG_WARN("No SX1280 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("SX1280 init success"); - radioType = SX1280_RADIO; - } - } -#endif - - // check if the radio chip matches the selected region - if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && rIf && (!rIf->wideLora())) { - 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"); - if (screen) { - screen->showSimpleBanner("Rebooting..."); - } - rebootAtMsec = millis() + 5000; - } - } + initLoRa(); lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation) @@ -1451,8 +1018,10 @@ void setup() #endif #if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) +#ifndef HAS_PHYSICAL_KEYBOARD osk_found = true; #endif +#endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER // Start web server thread. @@ -1501,13 +1070,15 @@ void setup() } #endif -uint32_t rebootAtMsec; // If not zero we will reboot at this time (used to reboot shortly after the update completes) -uint32_t shutdownAtMsec; // If not zero we will shutdown at this time (used to shutdown from python or mobile client) +uint32_t rebootAtMsec; // If not zero we will reboot at this time (used to reboot shortly after the update completes) +uint32_t shutdownAtMsec; // If not zero we will shutdown at this time (used to shutdown from python or mobile client) +bool suppressRebootBanner; // If true, suppress "Rebooting..." overlay (used for OTA handoff) // If a thread does something that might need for it to be rescheduled ASAP it can set this flag // This will suppress the current delay and instead try to run ASAP. bool runASAP; +// TODO find better home than main.cpp extern meshtastic_DeviceMetadata getDeviceMetadata() { meshtastic_DeviceMetadata deviceMetadata; @@ -1608,13 +1179,53 @@ void loop() if (inputBroker) inputBroker->processInputEventQueue(); #endif -#if ARCH_PORTDUINO && HAS_TFT +#if ARCH_PORTDUINO + if (portduino_config.lora_spi_dev == "ch341" && ch341Hal != nullptr) { + ch341Hal->checkError(); + } + if (portduino_status.LoRa_in_error && rebootAtMsec == 0) { + LOG_ERROR("LoRa in error detected, attempting to recover"); + if (rIf != nullptr) { + delete rIf; + rIf = nullptr; + } + if (portduino_config.lora_spi_dev == "ch341") { + if (ch341Hal != nullptr) { + delete ch341Hal; + ch341Hal = nullptr; + sleep(3); + } + try { + ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, + portduino_config.lora_usb_pid); + } catch (std::exception &e) { + std::cerr << e.what() << std::endl; + std::cerr << "Could not initialize CH341 device!" << std::endl; + exit(EXIT_FAILURE); + } + } + if (initLoRa()) { + router->addInterface(rIf); + portduino_status.LoRa_in_error = false; + } else { + LOG_WARN("Reconfigure failed, rebooting"); + if (screen) { + screen->showSimpleBanner("Rebooting..."); + } + rebootAtMsec = millis() + 25; + } + } +#if HAS_TFT if (screen && portduino_config.displayPanel == x11 && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { auto dispdev = screen->getDisplayDevice(); if (dispdev) static_cast(dispdev)->sdlLoop(); } +#endif +#endif +#if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE + messageStoreAutosaveTick(); #endif long delayMsec = mainController.runOrDelay(); diff --git a/src/main.h b/src/main.h index 414752b5c..91e27951f 100644 --- a/src/main.h +++ b/src/main.h @@ -26,8 +26,8 @@ extern NRF52Bluetooth *nrf52Bluetooth; #if ARCH_PORTDUINO extern HardwareSPI *DisplaySPI; extern HardwareSPI *LoraSPI; - #endif + extern ScanI2C::DeviceAddress screen_found; extern ScanI2C::DeviceAddress cardkb_found; extern uint8_t kb_model; @@ -42,21 +42,21 @@ extern bool eink_found; extern bool pmu_found; extern bool isUSBPowered; -#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) +#ifdef HAS_DRV2605 #include extern Adafruit_DRV2605 drv; #endif +#ifdef HAS_PCA9557 +#include +extern PCA9557 io; +#endif + #ifdef HAS_I2S #include "AudioThread.h" extern AudioThread *audioThread; #endif -#ifdef ELECROW_ThinkNode_M5 -#include -extern PCA9557 io; -#endif - #ifdef HAS_UDP_MULTICAST #include "mesh/udp/UdpMulticastHandler.h" extern UdpMulticastHandler *udpHandler; @@ -81,6 +81,7 @@ extern uint32_t timeLastPowered; extern uint32_t rebootAtMsec; extern uint32_t shutdownAtMsec; +extern bool suppressRebootBanner; extern uint32_t serialSinceMsec; diff --git a/src/mesh/Channels.h b/src/mesh/Channels.h index b53f552fa..a3cc7791c 100644 --- a/src/mesh/Channels.h +++ b/src/mesh/Channels.h @@ -96,6 +96,8 @@ class Channels bool setDefaultPresetCryptoForHash(ChannelHash channelHash); + int16_t getHash(ChannelIndex i) { return hashes[i]; } + private: /** Given a channel index, change to use the crypto key specified by that index * @@ -113,8 +115,6 @@ class Channels */ int16_t generateHash(ChannelIndex channelNum); - int16_t getHash(ChannelIndex i) { return hashes[i]; } - /** * Validate a channel, fixing any errors as needed */ diff --git a/src/mesh/CryptoEngine.cpp b/src/mesh/CryptoEngine.cpp index 9ca16878d..0f4d64113 100644 --- a/src/mesh/CryptoEngine.cpp +++ b/src/mesh/CryptoEngine.cpp @@ -61,11 +61,6 @@ bool CryptoEngine::regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey) return true; } #endif -void CryptoEngine::clearKeys() -{ - memset(public_key, 0, sizeof(public_key)); - memset(private_key, 0, sizeof(private_key)); -} /** * Encrypt a packet's payload using a key generated with Curve25519 and SHA256 diff --git a/src/mesh/CryptoEngine.h b/src/mesh/CryptoEngine.h index 6bbcb3b8a..7689006ab 100644 --- a/src/mesh/CryptoEngine.h +++ b/src/mesh/CryptoEngine.h @@ -37,7 +37,6 @@ class CryptoEngine virtual bool regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey); #endif - void clearKeys(); void setDHPrivateKey(uint8_t *_private_key); virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut); diff --git a/src/mesh/Default.h b/src/mesh/Default.h index a60e3af9b..e206d8277 100644 --- a/src/mesh/Default.h +++ b/src/mesh/Default.h @@ -13,7 +13,10 @@ #define min_default_telemetry_interval_secs 30 * 60 #define default_gps_update_interval IF_ROUTER(ONE_DAY, 2 * 60) #define default_telemetry_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60) -#define default_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 15 * 60) +#define default_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60) +#define default_broadcast_smart_minimum_interval_secs 5 * 60 +#define min_default_broadcast_interval_secs 60 * 60 +#define min_default_broadcast_smart_minimum_interval_secs 5 * 60 #define default_wait_bluetooth_secs IF_ROUTER(1, 60) #define default_sds_secs IF_ROUTER(ONE_DAY, UINT32_MAX) // Default to forever super deep sleep #define default_ls_secs IF_ROUTER(ONE_DAY, 5 * 60) diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index 032be241b..78602a9ec 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -4,6 +4,7 @@ #include "configuration.h" #include "mesh-pb-constants.h" #include "meshUtils.h" +#include "modules/TextMessageModule.h" #if !MESHTASTIC_EXCLUDE_TRACEROUTE #include "modules/TraceRouteModule.h" #endif @@ -35,6 +36,10 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) return true; // we handled it, so stop processing } + if (!seenRecently && !wasUpgraded && textMessageModule) { + seenRecently = textMessageModule->recentlySeen(p->id); + } + if (seenRecently) { printPacket("Ignore dupe incoming msg", p); rxDupe++; @@ -124,6 +129,10 @@ void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p) if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) { iface->clampToLateRebroadcastWindow(getFrom(p), p->id); } + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE && iface && nodeDB && + nodeDB->isFromOrToFavoritedNode(*p)) { + iface->clampToLateRebroadcastWindow(getFrom(p), p->id); + } } bool FloodingRouter::isRebroadcaster() diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index af6dd92e9..7c73b56cd 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -91,10 +91,21 @@ template bool LR11x0Interface::init() LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); #endif + // Allow extra time for TCXO to stabilize after power-on + delay(10); + int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + + // Retry if we get SPI command failed - some units need extra TCXO stabilization time + if (res == RADIOLIB_ERR_SPI_CMD_FAILED) { + LOG_WARN("LR11x0 init failed with %d (SPI_CMD_FAILED), retrying after delay...", res); + delay(100); + res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + } + // \todo Display actual typename of the adapter, not just `LR11x0` LOG_INFO("LR11x0 init result %d", res); - if (res == RADIOLIB_ERR_CHIP_NOT_FOUND) + if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) return false; LR11x0VersionInfo_t version; @@ -159,7 +170,7 @@ template bool LR11x0Interface::reconfigure() if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setCodingRate(cr); + err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); @@ -186,7 +197,7 @@ template bool LR11x0Interface::reconfigure() return RADIOLIB_ERR_NONE; } -template void INTERRUPT_ATTR LR11x0Interface::disableInterrupt() +template void LR11x0Interface::disableInterrupt() { lora.clearIrqAction(); } diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index c5748a560..83b64a873 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -195,7 +195,7 @@ void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src) // but opted NOT TO. Because it is not a good idea to let remote nodes 'probe' to find out which PSKs were "good" vs // bad. routingModule->sendAckNak(meshtastic_Routing_Error_NO_RESPONSE, getFrom(&mp), mp.id, mp.channel, - routingModule->getHopLimitForResponse(mp.hop_start, mp.hop_limit)); + routingModule->getHopLimitForResponse(mp)); } } @@ -235,7 +235,7 @@ void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to) assert(p->which_payload_variant == meshtastic_MeshPacket_decoded_tag); // Should already be set by now p->to = getFrom(&to); // Make sure that if we are sending to the local node, we use our local node addr, not 0 p->channel = to.channel; // Use the same channel that the request came in on - p->hop_limit = routingModule->getHopLimitForResponse(to.hop_start, to.hop_limit); + p->hop_limit = routingModule->getHopLimitForResponse(to); // No need for an ack if we are just delivering locally (it just generates an ignored ack) p->want_ack = (to.from != 0) ? to.want_ack : false; diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h index eda3f8881..63f401d18 100644 --- a/src/mesh/MeshModule.h +++ b/src/mesh/MeshModule.h @@ -44,6 +44,7 @@ struct UIFrameEvent { REDRAW_ONLY, // Don't change which frames are show, just redraw, asap REGENERATE_FRAMESET, // Regenerate (change? add? remove?) screen frames, honoring requestFocus() REGENERATE_FRAMESET_BACKGROUND, // Regenerate screen frames, Attempt to remain on the same frame throughout + SWITCH_TO_TEXTMESSAGE // Jump directly to the Text Message screen } action = REDRAW_ONLY; // We might want to pass additional data inside this struct at some point @@ -225,4 +226,4 @@ class MeshModule /** set the destination and packet parameters of packet p intended as a reply to a particular "to" packet * This ensures that if the request packet was sent reliably, the reply is sent that way as well. */ -void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to); \ No newline at end of file +void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to); diff --git a/src/mesh/MeshRadio.h b/src/mesh/MeshRadio.h index f2514eea1..bbb0ee00f 100644 --- a/src/mesh/MeshRadio.h +++ b/src/mesh/MeshRadio.h @@ -22,4 +22,99 @@ struct RegionInfo { extern const RegionInfo regions[]; extern const RegionInfo *myRegion; -extern void initRegion(); \ No newline at end of file +extern void initRegion(); + +static inline float bwCodeToKHz(uint16_t bwCode) +{ + if (bwCode == 31) + return 31.25f; + if (bwCode == 62) + return 62.5f; + if (bwCode == 200) + return 203.125f; + if (bwCode == 400) + return 406.25f; + if (bwCode == 800) + return 812.5f; + if (bwCode == 1600) + return 1625.0f; + return (float)bwCode; +} + +static inline uint16_t bwKHzToCode(float bwKHz) +{ + if (bwKHz > 31.24f && bwKHz < 31.26f) + return 31; + if (bwKHz > 62.49f && bwKHz < 62.51f) + return 62; + if (bwKHz > 203.12f && bwKHz < 203.13f) + return 200; + if (bwKHz > 406.24f && bwKHz < 406.26f) + return 400; + if (bwKHz > 812.49f && bwKHz < 812.51f) + return 800; + if (bwKHz > 1624.99f && bwKHz < 1625.01f) + return 1600; + return (uint16_t)(bwKHz + 0.5f); +} + +static inline void modemPresetToParams(meshtastic_Config_LoRaConfig_ModemPreset preset, bool wideLora, float &bwKHz, uint8_t &sf, + uint8_t &cr) +{ + switch (preset) { + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: + bwKHz = wideLora ? 1625.0f : 500.0f; + cr = 5; + sf = 7; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: + bwKHz = wideLora ? 812.5f : 250.0f; + cr = 5; + sf = 7; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: + bwKHz = wideLora ? 812.5f : 250.0f; + cr = 5; + sf = 8; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: + bwKHz = wideLora ? 812.5f : 250.0f; + cr = 5; + sf = 9; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: + bwKHz = wideLora ? 812.5f : 250.0f; + cr = 5; + sf = 10; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: + bwKHz = wideLora ? 1625.0f : 500.0f; + cr = 8; + sf = 11; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: + bwKHz = wideLora ? 406.25f : 125.0f; + cr = 8; + sf = 11; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: + bwKHz = wideLora ? 406.25f : 125.0f; + cr = 8; + sf = 12; + break; + default: // LONG_FAST (or illegal) + bwKHz = wideLora ? 812.5f : 250.0f; + cr = 5; + sf = 11; + break; + } +} + +static inline float modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset preset, bool wideLora) +{ + float bwKHz = 0; + uint8_t sf = 0; + uint8_t cr = 0; + modemPresetToParams(preset, wideLora, bwKHz, sf, cr); + return bwKHz; +} \ No newline at end of file diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 1b2af082d..c1b3839bb 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -7,10 +7,12 @@ #include "../concurrency/Periodic.h" #include "BluetoothCommon.h" // needed for updateBatteryLevel, FIXME, eventually when we pull mesh out into a lib we shouldn't be whacking bluetooth from here #include "MeshService.h" +#include "MessageStore.h" #include "NodeDB.h" #include "PowerFSM.h" #include "RTC.h" #include "TypeConversions.h" +#include "graphics/draw/MessageRenderer.h" #include "main.h" #include "mesh-pb-constants.h" #include "meshUtils.h" @@ -93,11 +95,8 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp) } else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user && nodeInfoModule && !isPreferredRebroadcaster && !nodeDB->isFull()) { if (airTime->isTxAllowedChannelUtil(true)) { - // Hops used by the request. If somebody in between running modified firmware modified it, ignore it - auto hopStart = mp->hop_start; - auto hopLimit = mp->hop_limit; - uint8_t hopsUsed = hopStart < hopLimit ? config.lora.hop_limit : hopStart - hopLimit; - if (hopsUsed > config.lora.hop_limit + 2) { + const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit); + if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) { LOG_DEBUG("Skip send NodeInfo: %d hops away is too far away", hopsUsed); } else { LOG_INFO("Heard new node on ch. %d, send NodeInfo and ask for response", mp->channel); @@ -192,8 +191,14 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p) p.id = generatePacketId(); // If the phone didn't supply one, then pick one p.rx_time = getValidTime(RTCQualityFromNet); // Record the time the packet arrived from the phone - // (so we update our nodedb for the local node) + IF_SCREEN(if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && p.decoded.payload.size > 0 && + p.to != NODENUM_BROADCAST && p.to != 0) // DM only + { + perhapsDecode(&p); + const StoredMessage &sm = messageStore.addFromPacket(p); + graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI + }) // Send the packet into the mesh DEBUG_HEAP_BEFORE; auto a = packetPool.allocCopy(p); @@ -276,6 +281,10 @@ bool MeshService::trySendPosition(NodeNum dest, bool wantReplies) if (nodeDB->hasValidPosition(node)) { #if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS if (positionModule) { + if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) { + LOG_DEBUG("Skip position ping; no fresh position since boot"); + return false; + } LOG_INFO("Send position ping to 0x%x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel); positionModule->sendOurPosition(dest, wantReplies, node->channel); return true; diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index afdb4d096..5230e5b85 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -64,7 +64,7 @@ bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) perhapsRebroadcast(p); } } else { - bool isRepeated = p->hop_start > 0 && p->hop_start == p->hop_limit; + bool isRepeated = getHopsAway(*p) == 0; // If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again if (isRepeated) { if (!findInTxQueue(p->from, p->id)) { @@ -101,8 +101,7 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast bool wasAlreadyRelayer = wasRelayer(p->relay_node, p->decoded.request_id, p->to); bool weWereSoleRelayer = false; bool weWereRelayer = wasRelayer(ourRelayID, p->decoded.request_id, p->to, &weWereSoleRelayer); - if ((weWereRelayer && wasAlreadyRelayer) || - (p->hop_start != 0 && p->hop_start == p->hop_limit && weWereSoleRelayer)) { + if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) { if (origTx->next_hop != p->relay_node) { // Not already set LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from, p->relay_node, wasAlreadyRelayer, weWereSoleRelayer); diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index d3000c500..0da4261b9 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -13,6 +13,7 @@ #include "PacketHistory.h" #include "PowerFSM.h" #include "RTC.h" +#include "RadioInterface.h" #include "Router.h" #include "SPILock.h" #include "SafeFile.h" @@ -26,6 +27,7 @@ #include #include #include +#include #include #ifdef ARCH_ESP32 @@ -53,7 +55,7 @@ #endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI -#include +#include #endif NodeDB *nodeDB = nullptr; @@ -335,6 +337,23 @@ NodeDB::NodeDB() moduleConfig.telemetry.health_update_interval = Default::getConfiguredOrMinimumValue( moduleConfig.telemetry.health_update_interval, min_default_telemetry_interval_secs); } + // Enforce position broadcast minimums if we would send positions over a default channel + // Check channels the same way PositionModule::sendOurPosition() does - first channel with position_precision set + bool positionUsesDefaultChannel = false; + for (uint8_t i = 0; i < channels.getNumChannels(); i++) { + if (channels.getByIndex(i).settings.has_module_settings && + channels.getByIndex(i).settings.module_settings.position_precision != 0) { + positionUsesDefaultChannel = channels.isDefaultChannel(i); + break; + } + } + if (positionUsesDefaultChannel) { + LOG_DEBUG("Coerce position broadcasts to min of 1 hour and smart broadcast min of 5 minutes on defaults"); + config.position.position_broadcast_secs = + Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, min_default_broadcast_interval_secs); + config.position.broadcast_smart_minimum_interval_secs = Default::getConfiguredOrMinimumValue( + config.position.broadcast_smart_minimum_interval_secs, min_default_broadcast_smart_minimum_interval_secs); + } // FIXME: UINT32_MAX intervals overflows Apple clients until they are fully patched if (config.device.node_info_broadcast_secs > MAX_INTERVAL) config.device.node_info_broadcast_secs = MAX_INTERVAL; @@ -644,7 +663,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.position.position_broadcast_smart_enabled = true; #endif config.position.broadcast_smart_minimum_distance = 100; - config.position.broadcast_smart_minimum_interval_secs = 30; + config.position.broadcast_smart_minimum_interval_secs = default_broadcast_smart_minimum_interval_secs; if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER) config.device.node_info_broadcast_secs = default_node_info_broadcast_secs; config.security.serial_enabled = true; @@ -739,8 +758,8 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.display.compass_orientation = COMPASS_ORIENTATION; #endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI - if (WiFiOTA::isUpdated()) { - WiFiOTA::recoverConfig(&config.network); + if (MeshtasticOTA::isUpdated()) { + MeshtasticOTA::recoverConfig(&config.network); } #endif @@ -805,11 +824,10 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.output_ms = 500; moduleConfig.external_notification.nag_timeout = 2; #endif -#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) - // Default to RAK led pin 2 (blue) +#if defined(LED_NOTIFICATION) moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.output = PIN_LED2; - moduleConfig.external_notification.active = true; + moduleConfig.external_notification.output = LED_NOTIFICATION; + moduleConfig.external_notification.active = LED_STATE_ON; moduleConfig.external_notification.alert_message = true; moduleConfig.external_notification.output_ms = 1000; moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; @@ -833,15 +851,6 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.output_ms = 100; moduleConfig.external_notification.active = true; #endif -#ifdef ELECROW_ThinkNode_M1 - // Default to Elecrow USER_LED (blue) - moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.output = USER_LED; - moduleConfig.external_notification.active = true; - moduleConfig.external_notification.alert_message = true; - moduleConfig.external_notification.output_ms = 1000; - moduleConfig.external_notification.nag_timeout = 60; -#endif #ifdef T_LORA_PAGER moduleConfig.canned_message.updown1_enabled = true; moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A; @@ -1039,6 +1048,7 @@ void NodeDB::clearLocalPosition() node->position.altitude = 0; node->position.time = 0; setLocalPosition(meshtastic_Position_init_default); + localPositionUpdatedSinceBoot = false; } void NodeDB::cleanupMeshDB() @@ -1241,6 +1251,23 @@ void NodeDB::loadFromDisk() if ((state != LoadFileResult::LOAD_SUCCESS) || (devicestate.version < DEVICESTATE_MIN_VER)) { LOG_WARN("Devicestate %d is old or invalid, discard", devicestate.version); installDefaultDeviceState(); + + // Attempt recovery of owner fields from our own NodeDB entry if available. + meshtastic_NodeInfoLite *us = getMeshNode(getNodeNum()); + if (us && us->has_user) { + LOG_WARN("Restoring owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for our node 0x%08x", + us->num); + memcpy(owner.long_name, us->user.long_name, sizeof(owner.long_name)); + owner.long_name[sizeof(owner.long_name) - 1] = '\0'; + memcpy(owner.short_name, us->user.short_name, sizeof(owner.short_name)); + owner.short_name[sizeof(owner.short_name) - 1] = '\0'; + owner.is_licensed = us->user.is_licensed; + owner.has_is_unmessagable = us->user.has_is_unmessagable; + owner.is_unmessagable = us->user.is_unmessagable; + + // Save the recovered owner to device state on disk + saveToDisk(SEGMENT_DEVICESTATE); + } } else { LOG_INFO("Loaded saved devicestate version %d", devicestate.version); } @@ -1257,6 +1284,13 @@ void NodeDB::loadFromDisk() LOG_INFO("Loaded saved config version %d", config.version); } } + + // Coerce LoRa config fields derived from presets while bootstrapping. + // Some clients/UI components display bandwidth/spread_factor directly from config even in preset mode. + if (config.has_lora && config.lora.use_preset) { + RadioInterface::bootstrapLoRaConfigFromPreset(config.lora); + } + if (backupSecurity.private_key.size > 0) { LOG_DEBUG("Restoring backup of security config"); config.security = backupSecurity; @@ -1370,6 +1404,15 @@ void NodeDB::loadFromDisk() if (portduino_config.has_configDisplayMode) { config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode; } + if (portduino_config.has_statusMessage) { + moduleConfig.has_statusmessage = true; + strncpy(moduleConfig.statusmessage.node_status, portduino_config.statusMessage.c_str(), + sizeof(moduleConfig.statusmessage.node_status)); + moduleConfig.statusmessage.node_status[sizeof(moduleConfig.statusmessage.node_status) - 1] = '\0'; + } + if (portduino_config.enable_UDP) { + config.network.enabled_protocols = true; + } #endif } @@ -1378,6 +1421,14 @@ void NodeDB::loadFromDisk() bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct, bool fullAtomic) { + + // do not try to save anything if power level is not safe. In many cases flash will be lock-protected + // and all writes will fail anyway. Device should be sleeping at this point anyway. + if (!powerHAL_isPowerLevelSafe()) { + LOG_ERROR("Error: trying to saveProto() on unsafe device power level."); + return false; + } + bool okay = false; #ifdef FSCom auto f = SafeFile(filename, fullAtomic); @@ -1404,6 +1455,14 @@ bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_ bool NodeDB::saveChannelsToDisk() { + + // do not try to save anything if power level is not safe. In many cases flash will be lock-protected + // and all writes will fail anyway. + if (!powerHAL_isPowerLevelSafe()) { + LOG_ERROR("Error: trying to saveChannelsToDisk() on unsafe device power level."); + return false; + } + #ifdef FSCom spiLock->lock(); FSCom.mkdir("/prefs"); @@ -1414,6 +1473,14 @@ bool NodeDB::saveChannelsToDisk() bool NodeDB::saveDeviceStateToDisk() { + + // do not try to save anything if power level is not safe. In many cases flash will be lock-protected + // and all writes will fail anyway. Device should be sleeping at this point anyway. + if (!powerHAL_isPowerLevelSafe()) { + LOG_ERROR("Error: trying to saveDeviceStateToDisk() on unsafe device power level."); + return false; + } + #ifdef FSCom spiLock->lock(); FSCom.mkdir("/prefs"); @@ -1426,6 +1493,14 @@ bool NodeDB::saveDeviceStateToDisk() bool NodeDB::saveNodeDatabaseToDisk() { + + // do not try to save anything if power level is not safe. In many cases flash will be lock-protected + // and all writes will fail anyway. Device should be sleeping at this point anyway. + if (!powerHAL_isPowerLevelSafe()) { + LOG_ERROR("Error: trying to saveNodeDatabaseToDisk() on unsafe device power level."); + return false; + } + #ifdef FSCom spiLock->lock(); FSCom.mkdir("/prefs"); @@ -1438,6 +1513,14 @@ bool NodeDB::saveNodeDatabaseToDisk() bool NodeDB::saveToDiskNoRetry(int saveWhat) { + + // do not try to save anything if power level is not safe. In many cases flash will be lock-protected + // and all writes will fail anyway. Device should be sleeping at this point anyway. + if (!powerHAL_isPowerLevelSafe()) { + LOG_ERROR("Error: trying to saveToDiskNoRetry() on unsafe device power level."); + return false; + } + bool success = true; #ifdef FSCom spiLock->lock(); @@ -1470,6 +1553,7 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat) moduleConfig.has_ambient_lighting = true; moduleConfig.has_audio = true; moduleConfig.has_paxcounter = true; + moduleConfig.has_statusmessage = true; success &= saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig); @@ -1493,6 +1577,14 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat) bool NodeDB::saveToDisk(int saveWhat) { LOG_DEBUG("Save to disk %d", saveWhat); + + // do not try to save anything if power level is not safe. In many cases flash will be lock-protected + // and all writes will fail anyway. Device should be sleeping at this point anyway. + if (!powerHAL_isPowerLevelSafe()) { + LOG_ERROR("Error: trying to saveToDisk() on unsafe device power level."); + return false; + } + bool success = saveToDiskNoRetry(saveWhat); if (!success) { @@ -1543,6 +1635,23 @@ uint32_t sinceReceived(const meshtastic_MeshPacket *p) return delta; } +int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown) +{ + // Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a + // bitfield that is always present. Use the presence of the bitfield to determine if the origin's firmware + // version is guaranteed to have hop_start populated. Note that this can only be done for decoded packets as + // the bitfield is encrypted under the channel encryption key. For encrypted packets, this returns + // defaultIfUnknown when hop_start is 0. + if (p.hop_start == 0 && !(p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield)) + return defaultIfUnknown; // Cannot reliably determine the number of hops. + + // Guard against invalid values. + if (p.hop_start < p.hop_limit) + return defaultIfUnknown; + + return p.hop_start - p.hop_limit; +} + #define NUM_ONLINE_SECS (60 * 60 * 2) // 2 hrs to consider someone offline size_t NodeDB::getNumOnlineMeshNodes(bool localOnly) @@ -1795,9 +1904,10 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) info->via_mqtt = mp.via_mqtt; // Store if we received this packet via MQTT // If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway - if (mp.hop_start != 0 && mp.hop_limit <= mp.hop_start) { + const int8_t hopsAway = getHopsAway(mp); + if (hopsAway >= 0) { info->has_hops_away = true; - info->hops_away = mp.hop_start - mp.hop_limit; + info->hops_away = hopsAway; } sortMeshDB(); } @@ -2132,7 +2242,10 @@ void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, co // Currently portuino is mostly used for simulation. Make sure the user notices something really bad happened #ifdef ARCH_PORTDUINO - LOG_ERROR("A critical failure occurred, portduino is exiting"); - exit(2); + LOG_ERROR("A critical failure occurred"); + // TODO: Determine if other critical errors should also cause an immediate exit + if (code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE || + code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE) + exit(2); #endif } diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 306acc0a5..adf2b42ea 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -110,6 +110,10 @@ uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n); /// Given a packet, return how many seconds in the past (vs now) it was received uint32_t sinceReceived(const meshtastic_MeshPacket *p); +/// Given a packet, return the number of hops used to reach this node. +/// Returns defaultIfUnknown if the number of hops couldn't be determined. +int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown = -1); + enum LoadFileResult { // Successfully opened the file LOAD_SUCCESS = 1, @@ -279,9 +283,13 @@ class NodeDB LOG_DEBUG("Set local position: lat=%i lon=%i time=%u timestamp=%u", position.latitude_i, position.longitude_i, position.time, position.timestamp); localPosition = position; + if (position.latitude_i != 0 || position.longitude_i != 0) { + localPositionUpdatedSinceBoot = true; + } } bool hasValidPosition(const meshtastic_NodeInfoLite *n); + bool hasLocalPositionSinceBoot() const { return localPositionUpdatedSinceBoot; } #if !defined(MESHTASTIC_EXCLUDE_PKI) bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest); @@ -301,6 +309,7 @@ class NodeDB private: bool duplicateWarned = false; + bool localPositionUpdatedSinceBoot = 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 uint32_t lastSort = 0; // When last sorted the nodeDB @@ -369,6 +378,8 @@ extern meshtastic_CriticalErrorCode error_code; extern uint32_t error_address; #define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT 0 #define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK (1 << NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT) +#define NODEINFO_BITFIELD_IS_MUTED_SHIFT 1 +#define NODEINFO_BITFIELD_IS_MUTED_MASK (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT) #define Module_Config_size \ (ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \ diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index da0039d38..0c12401ca 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -177,6 +177,9 @@ bool RF95Interface::init() int res = lora->begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength); LOG_INFO("RF95 init result %d", res); + if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) + return false; + LOG_INFO("Frequency set to %f", getFreq()); LOG_INFO("Bandwidth set to %f", bw); LOG_INFO("Power output set to %d", power); @@ -193,7 +196,7 @@ bool RF95Interface::init() return res == RADIOLIB_ERR_NONE; } -void INTERRUPT_ATTR RF95Interface::disableInterrupt() +void RF95Interface::disableInterrupt() { lora->clearDio0Action(); } diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 3c0da4494..bbd766329 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -1,17 +1,36 @@ #include "RadioInterface.h" #include "Channels.h" #include "DisplayFormatters.h" +#include "LLCC68Interface.h" +#include "LR1110Interface.h" +#include "LR1120Interface.h" +#include "LR1121Interface.h" #include "MeshRadio.h" #include "MeshService.h" #include "NodeDB.h" +#include "RF95Interface.h" #include "Router.h" +#include "SX1262Interface.h" +#include "SX1268Interface.h" +#include "SX1280Interface.h" #include "configuration.h" +#include "detect/LoRaRadioType.h" #include "main.h" #include "sleep.h" #include #include #include +#ifdef ARCH_PORTDUINO +#include "platform/portduino/PortduinoGlue.h" +#include "platform/portduino/SimRadio.h" +#include "platform/portduino/USBHal.h" +#endif + +#ifdef ARCH_STM32WL +#include "STM32WLE5JCInterface.h" +#endif + // Calculate 2^n without calling pow() uint32_t pow_of_2(uint32_t n) { @@ -171,7 +190,8 @@ const RegionInfo regions[] = { 863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields https://github.com/meshtastic/firmware/issues/7204 */ - RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false), RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, true), + RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false), + RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, false), /* Nepal @@ -204,6 +224,281 @@ bool RadioInterface::uses_default_frequency_slot = true; static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1]; +// Global LoRa radio type +LoRaRadioType radioType = NO_RADIO; + +extern RadioInterface *rIf; +extern RadioLibHal *RadioLibHAL; +#if defined(HW_SPI1_DEVICE) && defined(ARCH_ESP32) +extern SPIClass SPI1; +#endif + +bool initLoRa() +{ + if (rIf != nullptr) { + delete rIf; + rIf = nullptr; + } + +#if ARCH_PORTDUINO + SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); +#else + SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); +#endif + +#ifdef ARCH_PORTDUINO + // as one can't use a function pointer to the class constructor: + auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) { + switch (portduino_config.lora_module) { + case use_rf95: + return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy); + case use_sx1262: + return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy); + case use_sx1268: + return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy); + case use_sx1280: + return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy); + case use_lr1110: + return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy); + case use_lr1120: + return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy); + case use_lr1121: + return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy); + case use_llcc68: + return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy); + case use_simradio: + return (RadioInterface *)new SimRadio; + default: + assert(0); // shouldn't happen + return (RadioInterface *)nullptr; + } + }; + + LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(), + portduino_config.lora_spi_dev.c_str()); + if (portduino_config.lora_spi_dev == "ch341") { + RadioLibHAL = ch341Hal; + } else { + if (RadioLibHAL != nullptr) { + delete RadioLibHAL; + RadioLibHAL = nullptr; + } + RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); + } + rIf = + loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin, + portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin); + + if (!rIf->init()) { + LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str()); + delete rIf; + rIf = NULL; + exit(EXIT_FAILURE); + } else { + LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str()); + } + +#elif defined(HW_SPI1_DEVICE) + LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings); +#else // HW_SPI1_DEVICE + LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); +#endif + +// radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init) +#if defined(USE_STM32WLx) + if (!rIf) { + rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No STM32WL radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("STM32WL init success"); + radioType = STM32WLx_RADIO; + } + } +#endif + +#if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1 + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1); + if (!rIf->init()) { + LOG_WARN("No RF95 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("RF95 init success"); + radioType = RF95_RADIO; + } + } +#endif + +#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1 + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); +#ifdef SX126X_DIO3_TCXO_VOLTAGE + sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); +#endif + if (!sxIf->init()) { + LOG_WARN("No SX1262 radio"); + delete sxIf; + rIf = NULL; + } else { + LOG_INFO("SX1262 init success"); + rIf = sxIf; + radioType = SX1262_RADIO; + } + } +#endif + +#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL) + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + // try using the specified TCXO voltage + auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); + if (!sxIf->init()) { + LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + delete sxIf; + rIf = NULL; + } else { + LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + rIf = sxIf; + radioType = SX1262_RADIO; + } + } + + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + // If specified TCXO voltage fails, attempt to use DIO3 as a reference instead + rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("SX1262 init success, XTAL, Vref 0.0V"); + radioType = SX1262_RADIO; + } + } +#endif + +#if defined(USE_SX1268) +#if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL) + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + // try using the specified TCXO voltage + auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); + if (!sxIf->init()) { + LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + delete sxIf; + rIf = NULL; + } else { + LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + rIf = sxIf; + radioType = SX1268_RADIO; + } + } +#endif + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No SX1268 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("SX1268 init success"); + radioType = SX1268_RADIO; + } + } +#endif + +#if defined(USE_LLCC68) + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No LLCC68 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LLCC68 init success"); + radioType = LLCC68_RADIO; + } + } +#endif + +#if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1 + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN); + if (!rIf->init()) { + LOG_WARN("No LR1110 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LR1110 init success"); + radioType = LR1110_RADIO; + } + } +#endif + +#if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1 + if (!rIf) { + rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN); + if (!rIf->init()) { + LOG_WARN("No LR1120 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LR1120 init success"); + radioType = LR1120_RADIO; + } + } +#endif + +#if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1 + if (!rIf) { + rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN); + if (!rIf->init()) { + LOG_WARN("No LR1121 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LR1121 init success"); + radioType = LR1121_RADIO; + } + } +#endif + +#if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1 + if (!rIf) { + rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY); + if (!rIf->init()) { + LOG_WARN("No SX1280 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("SX1280 init success"); + radioType = SX1280_RADIO; + } + } +#endif + + // check if the radio chip matches the selected region + if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && rIf && (!rIf->wideLora())) { + 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 && !rIf->reconfigure()) { + LOG_WARN("Reconfigure failed, rebooting"); + if (screen) { + screen->showSimpleBanner("Rebooting..."); + } + rebootAtMsec = millis() + 5000; + } + } + return rIf != nullptr; +} + void initRegion() { const RegionInfo *r = regions; @@ -219,6 +514,34 @@ void initRegion() myRegion = r; } +void RadioInterface::bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig) +{ + if (!loraConfig.use_preset) { + return; + } + + // Find region info to determine whether "wide" LoRa is permitted (2.4 GHz uses wider bandwidth codes). + const RegionInfo *r = regions; + for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != loraConfig.region; r++) + ; + + const bool regionWideLora = r->wideLora; + + float bwKHz = 0; + uint8_t sf = 0; + uint8_t cr = 0; + modemPresetToParams(loraConfig.modem_preset, regionWideLora, bwKHz, sf, cr); + + // If selected preset requests a bandwidth larger than the region span, fall back to LONG_FAST. + if (r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && (r->freqEnd - r->freqStart) < (bwKHz / 1000.0f)) { + loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + modemPresetToParams(loraConfig.modem_preset, regionWideLora, bwKHz, sf, cr); + } + + loraConfig.bandwidth = bwKHzToCode(bwKHz); + loraConfig.spread_factor = sf; +} + /** * ## LoRaWAN for North America @@ -245,7 +568,9 @@ uint32_t RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool rece /** The delay to use for retransmitting dropped packets */ uint32_t RadioInterface::getRetransmissionMsec(const meshtastic_MeshPacket *p) { - size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); + size_t numbytes = p->which_payload_variant == meshtastic_MeshPacket_decoded_tag + ? pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded) + : p->encrypted.size + MESHTASTIC_HEADER_LENGTH; uint32_t packetAirtime = getPacketTime(numbytes + sizeof(PacketHeader)); // Make sure enough time has elapsed for this packet to be sent and an ACK is received. // LOG_DEBUG("Waiting for flooding message with airtime %d and slotTime is %d", packetAirtime, slotTimeMsec); @@ -296,11 +621,6 @@ bool RadioInterface::shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p) return true; } - // If we are a CLIENT_BASE and the packet is from or to a favorited node, we should rebroadcast early - if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { - return nodeDB->isFromOrToFavoritedNode(*p); - } - return false; } @@ -476,76 +796,38 @@ void RadioInterface::applyModemConfig() bool validConfig = false; // We need to check for a valid configuration while (!validConfig) { if (loraConfig.use_preset) { - - switch (loraConfig.modem_preset) { - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: - bw = (myRegion->wideLora) ? 1625.0 : 500; - cr = 5; - sf = 7; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 7; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 8; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 9; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 10; - break; - default: // Config_LoRaConfig_ModemPreset_LONG_FAST is default. Gracefully use this is preset is something illegal. - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 11; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: - bw = (myRegion->wideLora) ? 406.25 : 125; - cr = 8; - sf = 11; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: - bw = (myRegion->wideLora) ? 406.25 : 125; - cr = 8; - sf = 12; - break; + modemPresetToParams(loraConfig.modem_preset, myRegion->wideLora, bw, sf, cr); + if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) { + cr = loraConfig.coding_rate; + LOG_INFO("Using custom Coding Rate %u", cr); } } else { sf = loraConfig.spread_factor; cr = loraConfig.coding_rate; - bw = loraConfig.bandwidth; - - if (bw == 31) // This parameter is not an integer - bw = 31.25; - if (bw == 62) // Fix for 62.5Khz bandwidth - bw = 62.5; - if (bw == 200) - bw = 203.125; - if (bw == 400) - bw = 406.25; - if (bw == 800) - bw = 812.5; - if (bw == 1600) - bw = 1625.0; + bw = bwCodeToKHz(loraConfig.bandwidth); } if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) { - static const char *err_string = "Regional frequency range is smaller than bandwidth. Fall back to default preset"; - LOG_ERROR(err_string); + const float regionSpanKHz = (myRegion->freqEnd - myRegion->freqStart) * 1000.0f; + const float requestedBwKHz = bw; + const bool isWideRequest = requestedBwKHz >= 499.5f; // treat as 500 kHz preset + const char *presetName = + DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset); + + char err_string[160]; + if (isWideRequest) { + snprintf(err_string, sizeof(err_string), "%s region too narrow for 500kHz preset (%s). Falling back to LongFast.", + myRegion->name, presetName); + } else { + snprintf(err_string, sizeof(err_string), "%s region span %.0fkHz < requested %.0fkHz. Falling back to LongFast.", + myRegion->name, regionSpanKHz, requestedBwKHz); + } + LOG_ERROR("%s", err_string); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_ERROR; - sprintf(cn->message, err_string); + snprintf(cn->message, sizeof(cn->message), "%s", err_string); service->sendClientNotification(cn); // Set to default modem preset @@ -646,18 +928,24 @@ void RadioInterface::limitPower(int8_t loraMaxPower) power = maxPower; } -#ifndef NUM_PA_POINTS - if (TX_GAIN_LORA > 0 && !devicestate.owner.is_licensed) { - LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, TX_GAIN_LORA); - power -= TX_GAIN_LORA; - } +#ifdef ARCH_PORTDUINO + size_t num_pa_points = portduino_config.num_pa_points; + const uint16_t *tx_gain = portduino_config.tx_gain_lora; #else - if (!devicestate.owner.is_licensed) { + size_t num_pa_points = NUM_PA_POINTS; + const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA}; +#endif + + if (num_pa_points == 1) { + if (tx_gain[0] > 0 && !devicestate.owner.is_licensed) { + LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[0]); + power -= tx_gain[0]; + } + } else if (!devicestate.owner.is_licensed) { // we have an array of PA gain values. Find the highest power setting that works. - const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA}; - for (int radio_dbm = 0; radio_dbm < NUM_PA_POINTS; radio_dbm++) { + for (int radio_dbm = 0; radio_dbm < num_pa_points; radio_dbm++) { if (((radio_dbm + tx_gain[radio_dbm]) > power) || - ((radio_dbm == (NUM_PA_POINTS - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) { + ((radio_dbm == (num_pa_points - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) { // we've exceeded the power limit, or hit the max we can do LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[radio_dbm]); power -= tx_gain[radio_dbm]; @@ -665,7 +953,7 @@ void RadioInterface::limitPower(int8_t loraMaxPower) } } } -#endif + if (power > loraMaxPower) // Clamp power to maximum defined level power = loraMaxPower; diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 6049a11cc..cb092bc6d 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -7,6 +7,9 @@ #include "airtime.h" #include "error.h" +// Forward decl to avoid a direct include of generated config headers / full LoRaConfig definition in this widely-included file. +typedef struct _meshtastic_Config_LoRaConfig meshtastic_Config_LoRaConfig; + #define MAX_TX_QUEUE 16 // max number of packets which can be waiting for transmission #define MAX_LORA_PAYLOAD_LEN 255 // max length of 255 per Semtech's datasheets on SX12xx @@ -115,6 +118,12 @@ class RadioInterface virtual ~RadioInterface() {} + /** + * Coerce LoRa config fields (bandwidth/spread_factor) derived from presets. + * This is used during early bootstrapping so UIs that display these fields directly remain consistent. + */ + static void bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig); + /** * Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving) * @@ -270,5 +279,7 @@ class RadioInterface } }; +bool initLoRa(); + /// Debug printing for packets void printPacket(const char *prefix, const meshtastic_MeshPacket *p); diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 00066a7a3..42c24c783 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -1,6 +1,7 @@ #include "ReliableRouter.h" #include "Default.h" #include "MeshTypes.h" +#include "NodeDB.h" #include "configuration.h" #include "memGet.h" #include "mesh-pb-constants.h" @@ -16,12 +17,6 @@ ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p) { if (p->want_ack) { - // If someone asks for acks on broadcast, we need the hop limit to be at least one, so that first node that receives our - // message will rebroadcast. But asking for hop_limit 0 in that context means the client app has no preference on hop - // counts and we want this message to get through the whole mesh, so use the default. - if (p->hop_limit == 0) { - p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); - } DEBUG_HEAP_BEFORE; auto copy = packetPool.allocCopy(*p); DEBUG_HEAP_AFTER("ReliableRouter::send", copy); @@ -108,12 +103,12 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas // If this packet should always be ACKed reliably with want_ack back to the original sender, make sure we // do that unconditionally. sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, - routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit), true); + routingModule->getHopLimitForResponse(*p), true); } else if (!p->decoded.request_id && !p->decoded.reply_id) { // If it's not an ACK or a reply, send an ACK. sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, - routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit)); - } else if ((p->hop_start > 0 && p->hop_start == p->hop_limit) || p->next_hop != NO_NEXT_HOP_PREFERENCE) { + routingModule->getHopLimitForResponse(*p)); + } else if ((getHopsAway(*p) == 0) || p->next_hop != NO_NEXT_HOP_PREFERENCE) { // If we received the packet directly from the original sender, send a 0-hop ACK since the original sender // won't overhear any implicit ACKs. If we received the packet via NextHopRouter, also send a 0-hop ACK to // stop the immediate relayer's retransmissions. @@ -123,11 +118,11 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas (nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->user.public_key.size == 0)) { LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY"); sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(), - routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit)); + routingModule->getHopLimitForResponse(*p)); } else { // Send a 'NO_CHANNEL' error on the primary channel if want_ack packet destined for us cannot be decoded sendAckNak(meshtastic_Routing_Error_NO_CHANNEL, getFrom(p), p->id, channels.getPrimaryIndex(), - routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit)); + routingModule->getHopLimitForResponse(*p)); } } else if (p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum()) && p->hop_limit > 0) { // No wantAck, but we need to ACK with hop limit of 0 if we were the next hop to stop their retransmissions @@ -150,7 +145,9 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas PacketId nakId = (c && c->error_reason != meshtastic_Routing_Error_NONE) ? p->decoded.request_id : 0; // We intentionally don't check wasSeenRecently, because it is harmless to delete non existent retransmission records - if (ackId || nakId) { + if ((ackId || nakId) && + // Implicit ACKs from MQTT should not stop retransmissions + !(isFromUs(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT)) { LOG_DEBUG("Received a %s for 0x%x, stopping retransmissions", ackId ? "ACK" : "NAK", ackId); if (ackId) { stopRetransmission(p->to, ackId); diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 05f47d7f4..32544a051 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -7,7 +7,6 @@ #include "RTC.h" #include "configuration.h" -#include "detect/LoRaRadioType.h" #include "main.h" #include "mesh-pb-constants.h" #include "meshUtils.h" @@ -37,8 +36,8 @@ static MemoryDynamic dynamicPool; Allocator &packetPool = dynamicPool; -#elif defined(ARCH_STM32WL) -// On STM32 there isn't enough heap left over for the rest of the firmware if we allocate this statically. +#elif defined(ARCH_STM32WL) || defined(BOARD_HAS_PSRAM) +// On STM32 and boards with PSRAM, there isn't enough heap left over for the rest of the firmware if we allocate this statically. // For now, make it dynamic again. #define MAX_PACKETS \ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ @@ -81,8 +80,7 @@ Router::Router() : concurrency::OSThread("Router"), fromRadioQueue(MAX_RX_FROMRA bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p) { // First hop MUST always decrement to prevent retry issues - bool isFirstHop = (p->hop_start != 0 && p->hop_start == p->hop_limit); - if (isFirstHop) { + if (getHopsAway(*p) == 0) { return true; // Always decrement on first hop } @@ -114,7 +112,7 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p) // Check 3: role check (moderate cost - multiple comparisons) if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER, - meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { continue; } @@ -268,6 +266,13 @@ ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src) } } + // If someone asks for acks on broadcast, we need the hop limit to be at least one, so that first node that receives our + // message will rebroadcast. But asking for hop_limit 0 in that context means the client app has no preference on hop + // counts and we want this message to get through the whole mesh, so use the default. + if (src == RX_SRC_USER && p->want_ack && p->hop_limit == 0) { + p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); + } + return send(p); } } @@ -526,6 +531,10 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) #elif ARCH_PORTDUINO if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); + } else if (portduino_config.JSONFilename != "") { + if (portduino_config.JSONFilter == (_meshtastic_PortNum)0 || portduino_config.JSONFilter == p->decoded.portnum) { + JSONFile << MeshPacketSerializer::JsonSerialize(p, false) << std::endl; + } } #endif return DecodeState::DECODE_SUCCESS; @@ -611,15 +620,19 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) !(p->pki_encrypted != true && (strcasecmp(channels.getName(chIndex), Channels::serialChannel) == 0 || strcasecmp(channels.getName(chIndex), Channels::gpioChannel) == 0)) && // Check for valid keys and single node destination - config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr && - // Check for a known public key for the destination - (node->user.public_key.size == 32) && + config.security.private_key.size == 32 && !isBroadcast(p->to) && // Some portnums either make no sense to send with PKC p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP && p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) { LOG_DEBUG("Use PKI!"); if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN) return meshtastic_Routing_Error_TOO_LARGE; + // Check for a known public key for the destination + if (node == nullptr || node->user.public_key.size != 32) { + LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to, + p->decoded.portnum); + return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY; + } if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) && memcmp(p->public_key.bytes, node->user.public_key.bytes, 32) != 0) { LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes, @@ -688,7 +701,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) // Store a copy of encrypted packet for MQTT DEBUG_HEAP_BEFORE; - meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p); + p_encrypted = packetPool.allocCopy(*p); DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted); // Take those raw bytes and convert them back into a well structured protobuf we can understand @@ -726,7 +739,8 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_NODEINFO_APP, meshtastic_PortNum_ROUTING_APP, meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_ADMIN_APP, meshtastic_PortNum_ALERT_APP, meshtastic_PortNum_KEY_VERIFICATION_APP, meshtastic_PortNum_WAYPOINT_APP, - meshtastic_PortNum_STORE_FORWARD_APP, meshtastic_PortNum_TRACEROUTE_APP)) { + meshtastic_PortNum_STORE_FORWARD_APP, meshtastic_PortNum_TRACEROUTE_APP, + meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP)) { LOG_DEBUG("Ignore packet on non-standard portnum for CORE_PORTNUMS_ONLY"); cancelSending(p->from, p->id); skipHandle = true; @@ -741,19 +755,24 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) MeshModule::callModules(*p, src); #if !MESHTASTIC_EXCLUDE_MQTT - // Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM not to - // us (because we would be able to decrypt it) - if (decodedState == DecodeState::DECODE_FAILURE && moduleConfig.mqtt.encryption_enabled && p->channel == 0x00 && - !isBroadcast(p->to) && !isToUs(p)) - p_encrypted->pki_encrypted = true; - // After potentially altering it, publish received message to MQTT if we're not the original transmitter of the packet - if ((decodedState == DecodeState::DECODE_SUCCESS || p_encrypted->pki_encrypted) && moduleConfig.mqtt.enabled && - !isFromUs(p) && mqtt) - mqtt->onSend(*p_encrypted, *p, p->channel); + if (p_encrypted == nullptr) { + LOG_WARN("p_encrypted is null, skipping MQTT publish"); + } else { + // Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM not + // to us (because we would be able to decrypt it) + if (decodedState == DecodeState::DECODE_FAILURE && moduleConfig.mqtt.encryption_enabled && p->channel == 0x00 && + !isBroadcast(p->to) && !isToUs(p)) + p_encrypted->pki_encrypted = true; + // After potentially altering it, publish received message to MQTT if we're not the original transmitter of the packet + if ((decodedState == DecodeState::DECODE_SUCCESS || p_encrypted->pki_encrypted) && moduleConfig.mqtt.enabled && + !isFromUs(p) && mqtt) + mqtt->onSend(*p_encrypted, *p, p->channel); + } #endif } packetPool.release(p_encrypted); // Release the encrypted packet + p_encrypted = nullptr; } void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) diff --git a/src/mesh/Router.h b/src/mesh/Router.h index 10a3771a7..dbe6f4f39 100644 --- a/src/mesh/Router.h +++ b/src/mesh/Router.h @@ -91,6 +91,9 @@ class Router : protected concurrency::OSThread, protected PacketHistory before us */ uint32_t rxDupe = 0, txRelayCanceled = 0; + // pointer to the encrypted packet + meshtastic_MeshPacket *p_encrypted = nullptr; + protected: friend class RoutingModule; diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index e1f07a32b..9dfc46bee 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -53,13 +53,26 @@ template bool SX126xInterface::init() #endif #if defined(USE_GC1109_PA) + // GC1109 FEM chip initialization + // See variant.h for full pin mapping and control logic documentation + + // VFEM_Ctrl (LORA_PA_POWER): Power enable for GC1109 LDO (always on) pinMode(LORA_PA_POWER, OUTPUT); digitalWrite(LORA_PA_POWER, HIGH); + // CSD (LORA_PA_EN): Chip enable - must be HIGH to enable GC1109 for both RX and TX pinMode(LORA_PA_EN, OUTPUT); - digitalWrite(LORA_PA_EN, LOW); + digitalWrite(LORA_PA_EN, HIGH); + + // CPS (LORA_PA_TX_EN): PA mode select - HIGH enables full PA during TX, LOW for RX (don't care) + // Note: TX/RX path switching (CTX) is handled by DIO2 via SX126X_DIO2_AS_RF_SWITCH pinMode(LORA_PA_TX_EN, OUTPUT); - digitalWrite(LORA_PA_TX_EN, LOW); + digitalWrite(LORA_PA_TX_EN, LOW); // Start in RX-ready state +#endif + +#ifdef RF95_FAN_EN + digitalWrite(RF95_FAN_EN, HIGH); + pinMode(RF95_FAN_EN, OUTPUT); #endif #if ARCH_PORTDUINO @@ -85,6 +98,13 @@ template bool SX126xInterface::init() power = -9; int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO); + +#ifdef SX126X_PA_RAMP_US + // Set custom PA ramp time for boards requiring longer stabilization (e.g., T-Beam 1W needs >800us) + if (res == RADIOLIB_ERR_NONE) { + lora.setPaRampTime(SX126X_PA_RAMP_US); + } +#endif // \todo Display actual typename of the adapter, not just `SX126x` LOG_INFO("SX126x init result %d", res); if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) @@ -236,7 +256,7 @@ template bool SX126xInterface::reconfigure() return RADIOLIB_ERR_NONE; } -template void INTERRUPT_ATTR SX126xInterface::disableInterrupt() +template void SX126xInterface::disableInterrupt() { lora.clearDio1Action(); } @@ -249,8 +269,12 @@ template void SX126xInterface::setStandby() if (err != RADIOLIB_ERR_NONE) LOG_DEBUG("SX126x standby %s%d", radioLibErr, err); +#ifdef ARCH_PORTDUINO + if (err != RADIOLIB_ERR_NONE) + portduino_status.LoRa_in_error = true; +#else assert(err == RADIOLIB_ERR_NONE); - +#endif isReceiving = false; // If we were receiving, not any more activeReceiveStart = 0; disableInterrupt(); @@ -293,7 +317,12 @@ template void SX126xInterface::startReceive() int err = lora.startReceiveDutyCycleAuto(preambleLength, 8, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS); if (err != RADIOLIB_ERR_NONE) LOG_ERROR("SX126X startReceiveDutyCycleAuto %s%d", radioLibErr, err); +#ifdef ARCH_PORTDUINO + if (err != RADIOLIB_ERR_NONE) + portduino_status.LoRa_in_error = true; +#else assert(err == RADIOLIB_ERR_NONE); +#endif RadioLibInterface::startReceive(); @@ -321,7 +350,12 @@ template bool SX126xInterface::isChannelActive() return true; if (result != RADIOLIB_CHANNEL_FREE) LOG_ERROR("SX126X scanChannel %s%d", radioLibErr, result); +#ifdef ARCH_PORTDUINO + if (result == RADIOLIB_ERR_WRONG_MODEM) + portduino_status.LoRa_in_error = true; +#else assert(result != RADIOLIB_ERR_WRONG_MODEM); +#endif return false; } @@ -365,13 +399,13 @@ template bool SX126xInterface::sleep() return true; } -/** Some boards require GPIO control of tx vs rx paths */ +/** Control PA mode for GC1109 FEM - CPS pin selects full PA (txon=true) or bypass mode (txon=false) */ template void SX126xInterface::setTransmitEnable(bool txon) { #if defined(USE_GC1109_PA) - digitalWrite(LORA_PA_POWER, HIGH); - digitalWrite(LORA_PA_EN, HIGH); - digitalWrite(LORA_PA_TX_EN, txon ? 1 : 0); + digitalWrite(LORA_PA_POWER, HIGH); // Ensure LDO is on + digitalWrite(LORA_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_PA_TX_EN, txon ? 1 : 0); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) #endif } diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 80872af07..9fcedfe49 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -69,6 +69,8 @@ template bool SX128xInterface::init() int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength); // \todo Display actual typename of the adapter, not just `SX128x` LOG_INFO("SX128x init result %d", res); + if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) + return false; if ((config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && (res == RADIOLIB_ERR_INVALID_FREQUENCY)) { LOG_WARN("Radio only supports 2.4GHz LoRa. Adjusting Region and rebooting"); @@ -124,7 +126,7 @@ template bool SX128xInterface::reconfigure() if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setCodingRate(cr); + err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); @@ -155,7 +157,7 @@ template bool SX128xInterface::reconfigure() return RADIOLIB_ERR_NONE; } -template void INTERRUPT_ATTR SX128xInterface::disableInterrupt() +template void SX128xInterface::disableInterrupt() { lora.clearDio1Action(); } diff --git a/src/mesh/TypeConversions.cpp b/src/mesh/TypeConversions.cpp index 17cd92851..75195bd42 100644 --- a/src/mesh/TypeConversions.cpp +++ b/src/mesh/TypeConversions.cpp @@ -14,6 +14,7 @@ meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfo info.is_favorite = lite->is_favorite; info.is_ignored = lite->is_ignored; info.is_key_manually_verified = lite->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + info.is_muted = lite->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK; if (lite->has_hops_away) { info.has_hops_away = true; diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index 2b4f63512..a811ec16c 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -21,7 +21,7 @@ uint32_t ntp_renew = 0; #endif EthernetUDP syslogClient; -Syslog syslog(syslogClient); +meshtastic::Syslog syslog(syslogClient); bool ethStartupComplete = 0; diff --git a/src/mesh/generated/meshtastic/admin.pb.cpp b/src/mesh/generated/meshtastic/admin.pb.cpp index 4c4d0e3d1..01d3fa910 100644 --- a/src/mesh/generated/meshtastic/admin.pb.cpp +++ b/src/mesh/generated/meshtastic/admin.pb.cpp @@ -12,6 +12,9 @@ PB_BIND(meshtastic_AdminMessage, meshtastic_AdminMessage, 2) PB_BIND(meshtastic_AdminMessage_InputEvent, meshtastic_AdminMessage_InputEvent, AUTO) +PB_BIND(meshtastic_AdminMessage_OTAEvent, meshtastic_AdminMessage_OTAEvent, AUTO) + + PB_BIND(meshtastic_HamParameters, meshtastic_HamParameters, AUTO) @@ -24,6 +27,17 @@ PB_BIND(meshtastic_SharedContact, meshtastic_SharedContact, AUTO) PB_BIND(meshtastic_KeyVerificationAdmin, meshtastic_KeyVerificationAdmin, AUTO) +PB_BIND(meshtastic_SensorConfig, meshtastic_SensorConfig, AUTO) + + +PB_BIND(meshtastic_SCD4X_config, meshtastic_SCD4X_config, AUTO) + + +PB_BIND(meshtastic_SEN5X_config, meshtastic_SEN5X_config, AUTO) + + + + diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h index a542cf29c..f545ed9bf 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -16,6 +16,16 @@ #endif /* Enum definitions */ +/* Firmware update mode for OTA updates */ +typedef enum _meshtastic_OTAMode { + /* Do not reboot into OTA mode */ + meshtastic_OTAMode_NO_REBOOT_OTA = 0, + /* Reboot into OTA mode for BLE firmware update */ + meshtastic_OTAMode_OTA_BLE = 1, + /* Reboot into OTA mode for WiFi firmware update */ + meshtastic_OTAMode_OTA_WIFI = 2 +} meshtastic_OTAMode; + /* TODO: REPLACE */ typedef enum _meshtastic_AdminMessage_ConfigType { /* TODO: REPLACE */ @@ -67,7 +77,9 @@ typedef enum _meshtastic_AdminMessage_ModuleConfigType { /* TODO: REPLACE */ meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG = 11, /* TODO: REPLACE */ - meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG = 12 + meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG = 12, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG = 13 } meshtastic_AdminMessage_ModuleConfigType; typedef enum _meshtastic_AdminMessage_BackupLocation { @@ -103,6 +115,17 @@ typedef struct _meshtastic_AdminMessage_InputEvent { uint16_t touch_y; } meshtastic_AdminMessage_InputEvent; +typedef PB_BYTES_ARRAY_T(32) meshtastic_AdminMessage_OTAEvent_ota_hash_t; +/* User is requesting an over the air update. + Node will reboot into the OTA loader */ +typedef struct _meshtastic_AdminMessage_OTAEvent { + /* Tell the node to reboot into OTA mode for firmware update via BLE or WiFi (ESP32 only for now) */ + meshtastic_OTAMode reboot_ota_mode; + /* A 32 byte hash of the OTA firmware. + Used to verify the integrity of the firmware before applying an update. */ + meshtastic_AdminMessage_OTAEvent_ota_hash_t ota_hash; +} meshtastic_AdminMessage_OTAEvent; + /* Parameters for setting up Meshtastic for ameteur radio usage */ typedef struct _meshtastic_HamParameters { /* Amateur radio call sign, eg. KD2ABC */ @@ -148,6 +171,48 @@ typedef struct _meshtastic_KeyVerificationAdmin { uint32_t security_number; } meshtastic_KeyVerificationAdmin; +typedef struct _meshtastic_SCD4X_config { + /* Set Automatic self-calibration enabled */ + bool has_set_asc; + bool set_asc; + /* Recalibration target CO2 concentration in ppm (FRC or ASC) */ + bool has_set_target_co2_conc; + uint32_t set_target_co2_conc; + /* Reference temperature in degC */ + bool has_set_temperature; + float set_temperature; + /* Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure) */ + bool has_set_altitude; + uint32_t set_altitude; + /* Sensor ambient pressure in Pa. 70000 - 120000 Pa (overrides altitude) */ + bool has_set_ambient_pressure; + uint32_t set_ambient_pressure; + /* Perform a factory reset of the sensor */ + bool has_factory_reset; + bool factory_reset; + /* Power mode for sensor (true for low power, false for normal) */ + bool has_set_power_mode; + bool set_power_mode; +} meshtastic_SCD4X_config; + +typedef struct _meshtastic_SEN5X_config { + /* Reference temperature in degC */ + bool has_set_temperature; + float set_temperature; + /* One-shot mode (true for low power - one-shot mode, false for normal - continuous mode) */ + bool has_set_one_shot_mode; + bool set_one_shot_mode; +} meshtastic_SEN5X_config; + +typedef struct _meshtastic_SensorConfig { + /* SCD4X CO2 Sensor configuration */ + bool has_scd4x_config; + meshtastic_SCD4X_config scd4x_config; + /* SEN5X PM Sensor configuration */ + bool has_sen5x_config; + meshtastic_SEN5X_config sen5x_config; +} meshtastic_SensorConfig; + 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. @@ -249,6 +314,8 @@ typedef struct _meshtastic_AdminMessage { uint32_t set_ignored_node; /* Set specified node-num to be un-ignored on the NodeDB on the device */ uint32_t remove_ignored_node; + /* Set specified node-num to be muted */ + uint32_t toggle_muted_node; /* Begins an edit transaction for config, module config, owner, and channel settings changes This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) */ bool begin_edit_settings; @@ -261,7 +328,8 @@ typedef struct _meshtastic_AdminMessage { /* 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) - Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. */ + Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. + Deprecated in favor of reboot_ota_mode in 2.7.17 */ int32_t reboot_ota_seconds; /* This message is only supported for the simulator Portduino build. If received the simulator will exit successfully. */ @@ -275,6 +343,10 @@ typedef struct _meshtastic_AdminMessage { /* Tell the node to reset the nodedb. When true, favorites are preserved through reset. */ bool nodedb_reset; + /* Tell the node to reset into the OTA Loader */ + meshtastic_AdminMessage_OTAEvent ota_request; + /* Parameters and sensor configuration */ + meshtastic_SensorConfig sensor_config; }; /* The node generates this key and sends it with any get_x_response packets. The client MUST include the same key with any set_x commands. Key expires after 300 seconds. @@ -288,13 +360,17 @@ extern "C" { #endif /* Helper constants for enums */ +#define _meshtastic_OTAMode_MIN meshtastic_OTAMode_NO_REBOOT_OTA +#define _meshtastic_OTAMode_MAX meshtastic_OTAMode_OTA_WIFI +#define _meshtastic_OTAMode_ARRAYSIZE ((meshtastic_OTAMode)(meshtastic_OTAMode_OTA_WIFI+1)) + #define _meshtastic_AdminMessage_ConfigType_MIN meshtastic_AdminMessage_ConfigType_DEVICE_CONFIG #define _meshtastic_AdminMessage_ConfigType_MAX meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG #define _meshtastic_AdminMessage_ConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ConfigType)(meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG+1)) #define _meshtastic_AdminMessage_ModuleConfigType_MIN meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG -#define _meshtastic_AdminMessage_ModuleConfigType_MAX meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG -#define _meshtastic_AdminMessage_ModuleConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ModuleConfigType)(meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG+1)) +#define _meshtastic_AdminMessage_ModuleConfigType_MAX meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG +#define _meshtastic_AdminMessage_ModuleConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ModuleConfigType)(meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG+1)) #define _meshtastic_AdminMessage_BackupLocation_MIN meshtastic_AdminMessage_BackupLocation_FLASH #define _meshtastic_AdminMessage_BackupLocation_MAX meshtastic_AdminMessage_BackupLocation_SD @@ -311,31 +387,46 @@ extern "C" { #define meshtastic_AdminMessage_payload_variant_remove_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation +#define meshtastic_AdminMessage_OTAEvent_reboot_ota_mode_ENUMTYPE meshtastic_OTAMode + #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_AdminMessage_OTAEvent_init_default {_meshtastic_OTAMode_MIN, {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, 0, 0} #define meshtastic_KeyVerificationAdmin_init_default {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} +#define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default} +#define meshtastic_SCD4X_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_SEN5X_config_init_default {false, 0, false, 0} #define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}} #define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0} +#define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {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, 0, 0} #define meshtastic_KeyVerificationAdmin_init_zero {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} +#define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero} +#define meshtastic_SCD4X_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_SEN5X_config_init_zero {false, 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_AdminMessage_OTAEvent_reboot_ota_mode_tag 1 +#define meshtastic_AdminMessage_OTAEvent_ota_hash_tag 2 #define meshtastic_HamParameters_call_sign_tag 1 #define meshtastic_HamParameters_tx_power_tag 2 #define meshtastic_HamParameters_frequency_tag 3 @@ -349,6 +440,17 @@ extern "C" { #define meshtastic_KeyVerificationAdmin_remote_nodenum_tag 2 #define meshtastic_KeyVerificationAdmin_nonce_tag 3 #define meshtastic_KeyVerificationAdmin_security_number_tag 4 +#define meshtastic_SCD4X_config_set_asc_tag 1 +#define meshtastic_SCD4X_config_set_target_co2_conc_tag 2 +#define meshtastic_SCD4X_config_set_temperature_tag 3 +#define meshtastic_SCD4X_config_set_altitude_tag 4 +#define meshtastic_SCD4X_config_set_ambient_pressure_tag 5 +#define meshtastic_SCD4X_config_factory_reset_tag 6 +#define meshtastic_SCD4X_config_set_power_mode_tag 7 +#define meshtastic_SEN5X_config_set_temperature_tag 1 +#define meshtastic_SEN5X_config_set_one_shot_mode_tag 2 +#define meshtastic_SensorConfig_scd4x_config_tag 1 +#define meshtastic_SensorConfig_sen5x_config_tag 2 #define meshtastic_AdminMessage_get_channel_request_tag 1 #define meshtastic_AdminMessage_get_channel_response_tag 2 #define meshtastic_AdminMessage_get_owner_request_tag 3 @@ -392,6 +494,7 @@ extern "C" { #define meshtastic_AdminMessage_store_ui_config_tag 46 #define meshtastic_AdminMessage_set_ignored_node_tag 47 #define meshtastic_AdminMessage_remove_ignored_node_tag 48 +#define meshtastic_AdminMessage_toggle_muted_node_tag 49 #define meshtastic_AdminMessage_begin_edit_settings_tag 64 #define meshtastic_AdminMessage_commit_edit_settings_tag 65 #define meshtastic_AdminMessage_add_contact_tag 66 @@ -403,6 +506,8 @@ extern "C" { #define meshtastic_AdminMessage_shutdown_seconds_tag 98 #define meshtastic_AdminMessage_factory_reset_config_tag 99 #define meshtastic_AdminMessage_nodedb_reset_tag 100 +#define meshtastic_AdminMessage_ota_request_tag 102 +#define meshtastic_AdminMessage_sensor_config_tag 103 #define meshtastic_AdminMessage_session_passkey_tag 101 /* Struct field encoding specification for nanopb */ @@ -450,6 +555,7 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,get_ui_config_response,get_u X(a, STATIC, ONEOF, MESSAGE, (payload_variant,store_ui_config,store_ui_config), 46) \ X(a, STATIC, ONEOF, UINT32, (payload_variant,set_ignored_node,set_ignored_node), 47) \ X(a, STATIC, ONEOF, UINT32, (payload_variant,remove_ignored_node,remove_ignored_node), 48) \ +X(a, STATIC, ONEOF, UINT32, (payload_variant,toggle_muted_node,toggle_muted_node), 49) \ 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) \ @@ -461,7 +567,9 @@ X(a, STATIC, ONEOF, INT32, (payload_variant,reboot_seconds,reboot_second X(a, STATIC, ONEOF, INT32, (payload_variant,shutdown_seconds,shutdown_seconds), 98) \ X(a, STATIC, ONEOF, INT32, (payload_variant,factory_reset_config,factory_reset_config), 99) \ X(a, STATIC, ONEOF, BOOL, (payload_variant,nodedb_reset,nodedb_reset), 100) \ -X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) +X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ota_request,ota_request), 102) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,sensor_config,sensor_config), 103) #define meshtastic_AdminMessage_CALLBACK NULL #define meshtastic_AdminMessage_DEFAULT NULL #define meshtastic_AdminMessage_payload_variant_get_channel_response_MSGTYPE meshtastic_Channel @@ -482,6 +590,8 @@ X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) #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_payload_variant_ota_request_MSGTYPE meshtastic_AdminMessage_OTAEvent +#define meshtastic_AdminMessage_payload_variant_sensor_config_MSGTYPE meshtastic_SensorConfig #define meshtastic_AdminMessage_InputEvent_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, event_code, 1) \ @@ -491,6 +601,12 @@ X(a, STATIC, SINGULAR, UINT32, touch_y, 4) #define meshtastic_AdminMessage_InputEvent_CALLBACK NULL #define meshtastic_AdminMessage_InputEvent_DEFAULT NULL +#define meshtastic_AdminMessage_OTAEvent_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, reboot_ota_mode, 1) \ +X(a, STATIC, SINGULAR, BYTES, ota_hash, 2) +#define meshtastic_AdminMessage_OTAEvent_CALLBACK NULL +#define meshtastic_AdminMessage_OTAEvent_DEFAULT NULL + #define meshtastic_HamParameters_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, call_sign, 1) \ X(a, STATIC, SINGULAR, INT32, tx_power, 2) \ @@ -522,28 +638,65 @@ X(a, STATIC, OPTIONAL, UINT32, security_number, 4) #define meshtastic_KeyVerificationAdmin_CALLBACK NULL #define meshtastic_KeyVerificationAdmin_DEFAULT NULL +#define meshtastic_SensorConfig_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, scd4x_config, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, sen5x_config, 2) +#define meshtastic_SensorConfig_CALLBACK NULL +#define meshtastic_SensorConfig_DEFAULT NULL +#define meshtastic_SensorConfig_scd4x_config_MSGTYPE meshtastic_SCD4X_config +#define meshtastic_SensorConfig_sen5x_config_MSGTYPE meshtastic_SEN5X_config + +#define meshtastic_SCD4X_config_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \ +X(a, STATIC, OPTIONAL, UINT32, set_target_co2_conc, 2) \ +X(a, STATIC, OPTIONAL, FLOAT, set_temperature, 3) \ +X(a, STATIC, OPTIONAL, UINT32, set_altitude, 4) \ +X(a, STATIC, OPTIONAL, UINT32, set_ambient_pressure, 5) \ +X(a, STATIC, OPTIONAL, BOOL, factory_reset, 6) \ +X(a, STATIC, OPTIONAL, BOOL, set_power_mode, 7) +#define meshtastic_SCD4X_config_CALLBACK NULL +#define meshtastic_SCD4X_config_DEFAULT NULL + +#define meshtastic_SEN5X_config_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, FLOAT, set_temperature, 1) \ +X(a, STATIC, OPTIONAL, BOOL, set_one_shot_mode, 2) +#define meshtastic_SEN5X_config_CALLBACK NULL +#define meshtastic_SEN5X_config_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_AdminMessage_OTAEvent_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; +extern const pb_msgdesc_t meshtastic_SensorConfig_msg; +extern const pb_msgdesc_t meshtastic_SCD4X_config_msg; +extern const pb_msgdesc_t meshtastic_SEN5X_config_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_AdminMessage_OTAEvent_fields &meshtastic_AdminMessage_OTAEvent_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 +#define meshtastic_SensorConfig_fields &meshtastic_SensorConfig_msg +#define meshtastic_SCD4X_config_fields &meshtastic_SCD4X_config_msg +#define meshtastic_SEN5X_config_fields &meshtastic_SEN5X_config_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_OTAEvent_size 36 #define meshtastic_AdminMessage_size 511 #define meshtastic_HamParameters_size 31 #define meshtastic_KeyVerificationAdmin_size 25 #define meshtastic_NodeRemoteHardwarePinsResponse_size 496 +#define meshtastic_SCD4X_config_size 29 +#define meshtastic_SEN5X_config_size 7 +#define meshtastic_SensorConfig_size 40 #define meshtastic_SharedContact_size 127 #ifdef __cplusplus diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 327568316..d93f6fafa 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -66,7 +66,7 @@ typedef enum _meshtastic_Config_DeviceConfig_Role { but should not be given priority over other routers in order to avoid unnecessaraily consuming hops. */ meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11, - /* Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT. + /* Description: Treats packets from or to favorited nodes as ROUTER_LATE, and all other packets as CLIENT. Technical Details: Used for stronger attic/roof nodes to distribute messages more widely from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node. */ @@ -293,7 +293,8 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode { typedef enum _meshtastic_Config_LoRaConfig_ModemPreset { /* Long Range - Fast */ meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST = 0, - /* Long Range - Slow */ + /* Long Range - Slow + Deprecated in 2.7: Unpopular slow preset. */ meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW = 1, /* Very Long Range - Slow Deprecated in 2.5: Works only with txco and is unusably slow */ @@ -311,7 +312,10 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset { /* Short Range - Turbo This is the fastest preset and the only one with 500kHz bandwidth. It is not legal to use in all regions due to this wider bandwidth. */ - meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8 + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8, + /* Long Range - Turbo + This preset performs similarly to LongFast, but with 500Khz bandwidth. */ + meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9 } meshtastic_Config_LoRaConfig_ModemPreset; typedef enum _meshtastic_Config_BluetoothConfig_PairingMode { @@ -689,8 +693,8 @@ extern "C" { #define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_BR_902+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 -#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO+1)) +#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO +#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO+1)) #define _meshtastic_Config_BluetoothConfig_PairingMode_MIN meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN #define _meshtastic_Config_BluetoothConfig_PairingMode_MAX meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index 7fab82ff7..57e7df8fc 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -97,7 +97,8 @@ typedef struct _meshtastic_NodeInfoLite { /* Last byte of the node number of the node that should be used as the next hop to reach this node. */ uint8_t next_hop; /* Bitfield for storing booleans. - LSB 0 is_key_manually_verified */ + LSB 0 is_key_manually_verified + LSB 1 is_muted */ uint32_t bitfield; } meshtastic_NodeInfoLite; @@ -360,7 +361,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 2277 +#define meshtastic_BackupPreferences_size 2362 #define meshtastic_ChannelFile_size 718 #define meshtastic_DeviceState_size 1737 #define meshtastic_NodeInfoLite_size 196 diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h index 3ab6f02c1..f11b13419 100644 --- a/src/mesh/generated/meshtastic/localonly.pb.h +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -87,6 +87,9 @@ typedef struct _meshtastic_LocalModuleConfig { /* Paxcounter Config */ bool has_paxcounter; meshtastic_ModuleConfig_PaxcounterConfig paxcounter; + /* StatusMessage Config */ + bool has_statusmessage; + meshtastic_ModuleConfig_StatusMessageConfig statusmessage; } meshtastic_LocalModuleConfig; @@ -96,9 +99,9 @@ extern "C" { /* Initializer values for message structs */ #define meshtastic_LocalConfig_init_default {false, meshtastic_Config_DeviceConfig_init_default, false, meshtastic_Config_PositionConfig_init_default, false, meshtastic_Config_PowerConfig_init_default, false, meshtastic_Config_NetworkConfig_init_default, false, meshtastic_Config_DisplayConfig_init_default, false, meshtastic_Config_LoRaConfig_init_default, false, meshtastic_Config_BluetoothConfig_init_default, 0, false, meshtastic_Config_SecurityConfig_init_default} -#define meshtastic_LocalModuleConfig_init_default {false, meshtastic_ModuleConfig_MQTTConfig_init_default, false, meshtastic_ModuleConfig_SerialConfig_init_default, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_default, false, meshtastic_ModuleConfig_StoreForwardConfig_init_default, false, meshtastic_ModuleConfig_RangeTestConfig_init_default, false, meshtastic_ModuleConfig_TelemetryConfig_init_default, false, meshtastic_ModuleConfig_CannedMessageConfig_init_default, 0, false, meshtastic_ModuleConfig_AudioConfig_init_default, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_default, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_default, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_default, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_default, false, meshtastic_ModuleConfig_PaxcounterConfig_init_default} +#define meshtastic_LocalModuleConfig_init_default {false, meshtastic_ModuleConfig_MQTTConfig_init_default, false, meshtastic_ModuleConfig_SerialConfig_init_default, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_default, false, meshtastic_ModuleConfig_StoreForwardConfig_init_default, false, meshtastic_ModuleConfig_RangeTestConfig_init_default, false, meshtastic_ModuleConfig_TelemetryConfig_init_default, false, meshtastic_ModuleConfig_CannedMessageConfig_init_default, 0, false, meshtastic_ModuleConfig_AudioConfig_init_default, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_default, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_default, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_default, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_default, false, meshtastic_ModuleConfig_PaxcounterConfig_init_default, false, meshtastic_ModuleConfig_StatusMessageConfig_init_default} #define meshtastic_LocalConfig_init_zero {false, meshtastic_Config_DeviceConfig_init_zero, false, meshtastic_Config_PositionConfig_init_zero, false, meshtastic_Config_PowerConfig_init_zero, false, meshtastic_Config_NetworkConfig_init_zero, false, meshtastic_Config_DisplayConfig_init_zero, false, meshtastic_Config_LoRaConfig_init_zero, false, meshtastic_Config_BluetoothConfig_init_zero, 0, false, meshtastic_Config_SecurityConfig_init_zero} -#define meshtastic_LocalModuleConfig_init_zero {false, meshtastic_ModuleConfig_MQTTConfig_init_zero, false, meshtastic_ModuleConfig_SerialConfig_init_zero, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero, false, meshtastic_ModuleConfig_StoreForwardConfig_init_zero, false, meshtastic_ModuleConfig_RangeTestConfig_init_zero, false, meshtastic_ModuleConfig_TelemetryConfig_init_zero, false, meshtastic_ModuleConfig_CannedMessageConfig_init_zero, 0, false, meshtastic_ModuleConfig_AudioConfig_init_zero, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_zero, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_zero, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_zero, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_zero, false, meshtastic_ModuleConfig_PaxcounterConfig_init_zero} +#define meshtastic_LocalModuleConfig_init_zero {false, meshtastic_ModuleConfig_MQTTConfig_init_zero, false, meshtastic_ModuleConfig_SerialConfig_init_zero, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero, false, meshtastic_ModuleConfig_StoreForwardConfig_init_zero, false, meshtastic_ModuleConfig_RangeTestConfig_init_zero, false, meshtastic_ModuleConfig_TelemetryConfig_init_zero, false, meshtastic_ModuleConfig_CannedMessageConfig_init_zero, 0, false, meshtastic_ModuleConfig_AudioConfig_init_zero, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_zero, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_zero, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_zero, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_zero, false, meshtastic_ModuleConfig_PaxcounterConfig_init_zero, false, meshtastic_ModuleConfig_StatusMessageConfig_init_zero} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_LocalConfig_device_tag 1 @@ -124,6 +127,7 @@ extern "C" { #define meshtastic_LocalModuleConfig_ambient_lighting_tag 12 #define meshtastic_LocalModuleConfig_detection_sensor_tag 13 #define meshtastic_LocalModuleConfig_paxcounter_tag 14 +#define meshtastic_LocalModuleConfig_statusmessage_tag 15 /* Struct field encoding specification for nanopb */ #define meshtastic_LocalConfig_FIELDLIST(X, a) \ @@ -161,7 +165,8 @@ X(a, STATIC, OPTIONAL, MESSAGE, remote_hardware, 10) \ X(a, STATIC, OPTIONAL, MESSAGE, neighbor_info, 11) \ X(a, STATIC, OPTIONAL, MESSAGE, ambient_lighting, 12) \ X(a, STATIC, OPTIONAL, MESSAGE, detection_sensor, 13) \ -X(a, STATIC, OPTIONAL, MESSAGE, paxcounter, 14) +X(a, STATIC, OPTIONAL, MESSAGE, paxcounter, 14) \ +X(a, STATIC, OPTIONAL, MESSAGE, statusmessage, 15) #define meshtastic_LocalModuleConfig_CALLBACK NULL #define meshtastic_LocalModuleConfig_DEFAULT NULL #define meshtastic_LocalModuleConfig_mqtt_MSGTYPE meshtastic_ModuleConfig_MQTTConfig @@ -177,6 +182,7 @@ X(a, STATIC, OPTIONAL, MESSAGE, paxcounter, 14) #define meshtastic_LocalModuleConfig_ambient_lighting_MSGTYPE meshtastic_ModuleConfig_AmbientLightingConfig #define meshtastic_LocalModuleConfig_detection_sensor_MSGTYPE meshtastic_ModuleConfig_DetectionSensorConfig #define meshtastic_LocalModuleConfig_paxcounter_MSGTYPE meshtastic_ModuleConfig_PaxcounterConfig +#define meshtastic_LocalModuleConfig_statusmessage_MSGTYPE meshtastic_ModuleConfig_StatusMessageConfig extern const pb_msgdesc_t meshtastic_LocalConfig_msg; extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; @@ -186,9 +192,9 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; #define meshtastic_LocalModuleConfig_fields &meshtastic_LocalModuleConfig_msg /* Maximum encoded size of messages (where known) */ -#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalConfig_size +#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size #define meshtastic_LocalConfig_size 749 -#define meshtastic_LocalModuleConfig_size 673 +#define meshtastic_LocalModuleConfig_size 758 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/mesh.pb.cpp b/src/mesh/generated/meshtastic/mesh.pb.cpp index 9966e52f8..7f1a738c6 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.cpp +++ b/src/mesh/generated/meshtastic/mesh.pb.cpp @@ -24,9 +24,15 @@ PB_BIND(meshtastic_Data, meshtastic_Data, 2) PB_BIND(meshtastic_KeyVerification, meshtastic_KeyVerification, AUTO) +PB_BIND(meshtastic_StoreForwardPlusPlus, meshtastic_StoreForwardPlusPlus, 2) + + PB_BIND(meshtastic_Waypoint, meshtastic_Waypoint, AUTO) +PB_BIND(meshtastic_StatusMessage, meshtastic_StatusMessage, AUTO) + + PB_BIND(meshtastic_MqttClientProxyMessage, meshtastic_MqttClientProxyMessage, 2) @@ -121,6 +127,8 @@ PB_BIND(meshtastic_ChunkedPayloadResponse, meshtastic_ChunkedPayloadResponse, AU + + diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 0c48a7891..aeae4bd84 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -92,8 +92,8 @@ typedef enum _meshtastic_HardwareModel { Less common/prototype boards listed here (needs one more byte over the air) --------------------------------------------------------------------------- */ meshtastic_HardwareModel_LORA_RELAY_V1 = 32, - /* TODO: REPLACE */ - meshtastic_HardwareModel_NRF52840DK = 33, + /* T-Echo Plus device from LilyGo */ + meshtastic_HardwareModel_T_ECHO_PLUS = 33, /* TODO: REPLACE */ meshtastic_HardwareModel_PPR = 34, /* TODO: REPLACE */ @@ -294,6 +294,12 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_THINKNODE_M4 = 119, /* Elecrow ThinkNode M6 */ meshtastic_HardwareModel_THINKNODE_M6 = 120, + /* Elecrow Meshstick 1262 */ + meshtastic_HardwareModel_MESHSTICK_1262 = 121, + /* LilyGo T-Beam 1W */ + meshtastic_HardwareModel_TBEAM_1_WATT = 122, + /* LilyGo T5 S3 ePaper Pro (V1 and V2) */ + meshtastic_HardwareModel_T5_S3_EPAPER_PRO = 123, /* ------------------------------------------------------------------------------------------------------------------------------------------ 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. ------------------------------------------------------------------------------------------------------------------------------------------ */ @@ -475,9 +481,28 @@ typedef enum _meshtastic_Routing_Error { meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED = 37, /* Airtime fairness rate limit exceeded for a packet This typically enforced per portnum and is used to prevent a single node from monopolizing airtime */ - meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED = 38 + meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED = 38, + /* PKI encryption failed, due to no public key for the remote node + This is different from PKI_UNKNOWN_PUBKEY which indicates a failure upon receiving a packet */ + meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY = 39 } meshtastic_Routing_Error; +/* Enum of message types */ +typedef enum _meshtastic_StoreForwardPlusPlus_SFPP_message_type { + /* Send an announcement of the canonical tip of a chain */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE = 0, + /* Query whether a specific link is on the chain */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_CHAIN_QUERY = 1, + /* Request the next link in the chain */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_REQUEST = 3, + /* Provide a link to add to the chain */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE = 4, + /* If we must fragment, send the first half */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_FIRSTHALF = 5, + /* If we must fragment, send the second half */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF = 6 +} meshtastic_StoreForwardPlusPlus_SFPP_message_type; + /* The priority of this message for sending. Higher priorities are sent first (when managing the transmit queue). This field is never sent over the air, it is only used internally inside of a local device node. @@ -782,6 +807,34 @@ typedef struct _meshtastic_KeyVerification { meshtastic_KeyVerification_hash2_t hash2; } meshtastic_KeyVerification; +typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_message_hash_t; +typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_commit_hash_t; +typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_root_hash_t; +typedef PB_BYTES_ARRAY_T(240) meshtastic_StoreForwardPlusPlus_message_t; +/* The actual over-the-mesh message doing store and forward++ */ +typedef struct _meshtastic_StoreForwardPlusPlus { + /* Which message type is this */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type sfpp_message_type; + /* The hash of the specific message */ + meshtastic_StoreForwardPlusPlus_message_hash_t message_hash; + /* The hash of a link on a chain */ + meshtastic_StoreForwardPlusPlus_commit_hash_t commit_hash; + /* the root hash of a chain */ + meshtastic_StoreForwardPlusPlus_root_hash_t root_hash; + /* The encrypted bytes from a message */ + meshtastic_StoreForwardPlusPlus_message_t message; + /* Message ID of the contained message */ + uint32_t encapsulated_id; + /* Destination of the contained message */ + uint32_t encapsulated_to; + /* Sender of the contained message */ + uint32_t encapsulated_from; + /* The receive time of the message in question */ + uint32_t encapsulated_rxtime; + /* Used in a LINK_REQUEST to specify the message X spots back from head */ + uint32_t chain_count; +} meshtastic_StoreForwardPlusPlus; + /* Waypoint message, used to share arbitrary locations across the mesh */ typedef struct _meshtastic_Waypoint { /* Id of the waypoint */ @@ -805,6 +858,11 @@ typedef struct _meshtastic_Waypoint { uint32_t icon; } meshtastic_Waypoint; +/* Message for node status */ +typedef struct _meshtastic_StatusMessage { + char status[80]; +} meshtastic_StatusMessage; + typedef PB_BYTES_ARRAY_T(435) meshtastic_MqttClientProxyMessage_data_t; /* This message will be proxied over the PhoneAPI for the client to deliver to the MQTT server */ typedef struct _meshtastic_MqttClientProxyMessage { @@ -966,6 +1024,9 @@ typedef struct _meshtastic_NodeInfo { Persists between NodeDB internal clean ups LSB 0 of the bitfield */ bool is_key_manually_verified; + /* True if node has been muted + Persistes between NodeDB internal clean ups */ + bool is_muted; } meshtastic_NodeInfo; typedef PB_BYTES_ARRAY_T(16) meshtastic_MyNodeInfo_device_id_t; @@ -1307,8 +1368,12 @@ extern "C" { #define _meshtastic_Position_AltSource_ARRAYSIZE ((meshtastic_Position_AltSource)(meshtastic_Position_AltSource_ALT_BAROMETRIC+1)) #define _meshtastic_Routing_Error_MIN meshtastic_Routing_Error_NONE -#define _meshtastic_Routing_Error_MAX meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED -#define _meshtastic_Routing_Error_ARRAYSIZE ((meshtastic_Routing_Error)(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED+1)) +#define _meshtastic_Routing_Error_MAX meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY +#define _meshtastic_Routing_Error_ARRAYSIZE ((meshtastic_Routing_Error)(meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY+1)) + +#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE +#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MAX meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF +#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_ARRAYSIZE ((meshtastic_StoreForwardPlusPlus_SFPP_message_type)(meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF+1)) #define _meshtastic_MeshPacket_Priority_MIN meshtastic_MeshPacket_Priority_UNSET #define _meshtastic_MeshPacket_Priority_MAX meshtastic_MeshPacket_Priority_MAX @@ -1338,6 +1403,9 @@ extern "C" { #define meshtastic_Data_portnum_ENUMTYPE meshtastic_PortNum +#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_ENUMTYPE meshtastic_StoreForwardPlusPlus_SFPP_message_type + + #define meshtastic_MeshPacket_priority_ENUMTYPE meshtastic_MeshPacket_Priority @@ -1380,10 +1448,12 @@ extern "C" { #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_StoreForwardPlusPlus_init_default {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0} #define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0} +#define meshtastic_StatusMessage_init_default {""} #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, _meshtastic_MeshPacket_TransportMechanism_MIN} -#define meshtastic_NodeInfo_init_default {0, false, meshtastic_User_init_default, false, meshtastic_Position_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0} +#define meshtastic_NodeInfo_init_default {0, false, meshtastic_User_init_default, false, meshtastic_Position_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0, 0} #define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0} #define meshtastic_LogRecord_init_default {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_default {0, 0, 0, 0} @@ -1411,10 +1481,12 @@ extern "C" { #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_StoreForwardPlusPlus_init_zero {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0} #define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0} +#define meshtastic_StatusMessage_init_zero {""} #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, _meshtastic_MeshPacket_TransportMechanism_MIN} -#define meshtastic_NodeInfo_init_zero {0, false, meshtastic_User_init_zero, false, meshtastic_Position_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0} +#define meshtastic_NodeInfo_init_zero {0, false, meshtastic_User_init_zero, false, meshtastic_Position_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0, 0} #define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0} #define meshtastic_LogRecord_init_zero {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_zero {0, 0, 0, 0} @@ -1489,6 +1561,16 @@ extern "C" { #define meshtastic_KeyVerification_nonce_tag 1 #define meshtastic_KeyVerification_hash1_tag 2 #define meshtastic_KeyVerification_hash2_tag 3 +#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_tag 1 +#define meshtastic_StoreForwardPlusPlus_message_hash_tag 2 +#define meshtastic_StoreForwardPlusPlus_commit_hash_tag 3 +#define meshtastic_StoreForwardPlusPlus_root_hash_tag 4 +#define meshtastic_StoreForwardPlusPlus_message_tag 5 +#define meshtastic_StoreForwardPlusPlus_encapsulated_id_tag 6 +#define meshtastic_StoreForwardPlusPlus_encapsulated_to_tag 7 +#define meshtastic_StoreForwardPlusPlus_encapsulated_from_tag 8 +#define meshtastic_StoreForwardPlusPlus_encapsulated_rxtime_tag 9 +#define meshtastic_StoreForwardPlusPlus_chain_count_tag 10 #define meshtastic_Waypoint_id_tag 1 #define meshtastic_Waypoint_latitude_i_tag 2 #define meshtastic_Waypoint_longitude_i_tag 3 @@ -1497,6 +1579,7 @@ extern "C" { #define meshtastic_Waypoint_name_tag 6 #define meshtastic_Waypoint_description_tag 7 #define meshtastic_Waypoint_icon_tag 8 +#define meshtastic_StatusMessage_status_tag 1 #define meshtastic_MqttClientProxyMessage_topic_tag 1 #define meshtastic_MqttClientProxyMessage_data_tag 2 #define meshtastic_MqttClientProxyMessage_text_tag 3 @@ -1534,6 +1617,7 @@ extern "C" { #define meshtastic_NodeInfo_is_favorite_tag 10 #define meshtastic_NodeInfo_is_ignored_tag 11 #define meshtastic_NodeInfo_is_key_manually_verified_tag 12 +#define meshtastic_NodeInfo_is_muted_tag 13 #define meshtastic_MyNodeInfo_my_node_num_tag 1 #define meshtastic_MyNodeInfo_reboot_count_tag 8 #define meshtastic_MyNodeInfo_min_app_version_tag 11 @@ -1705,6 +1789,20 @@ X(a, STATIC, SINGULAR, BYTES, hash2, 3) #define meshtastic_KeyVerification_CALLBACK NULL #define meshtastic_KeyVerification_DEFAULT NULL +#define meshtastic_StoreForwardPlusPlus_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, sfpp_message_type, 1) \ +X(a, STATIC, SINGULAR, BYTES, message_hash, 2) \ +X(a, STATIC, SINGULAR, BYTES, commit_hash, 3) \ +X(a, STATIC, SINGULAR, BYTES, root_hash, 4) \ +X(a, STATIC, SINGULAR, BYTES, message, 5) \ +X(a, STATIC, SINGULAR, UINT32, encapsulated_id, 6) \ +X(a, STATIC, SINGULAR, UINT32, encapsulated_to, 7) \ +X(a, STATIC, SINGULAR, UINT32, encapsulated_from, 8) \ +X(a, STATIC, SINGULAR, UINT32, encapsulated_rxtime, 9) \ +X(a, STATIC, SINGULAR, UINT32, chain_count, 10) +#define meshtastic_StoreForwardPlusPlus_CALLBACK NULL +#define meshtastic_StoreForwardPlusPlus_DEFAULT NULL + #define meshtastic_Waypoint_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, id, 1) \ X(a, STATIC, OPTIONAL, SFIXED32, latitude_i, 2) \ @@ -1717,6 +1815,11 @@ X(a, STATIC, SINGULAR, FIXED32, icon, 8) #define meshtastic_Waypoint_CALLBACK NULL #define meshtastic_Waypoint_DEFAULT NULL +#define meshtastic_StatusMessage_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, status, 1) +#define meshtastic_StatusMessage_CALLBACK NULL +#define meshtastic_StatusMessage_DEFAULT NULL + #define meshtastic_MqttClientProxyMessage_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, topic, 1) \ X(a, STATIC, ONEOF, BYTES, (payload_variant,data,payload_variant.data), 2) \ @@ -1763,7 +1866,8 @@ X(a, STATIC, SINGULAR, BOOL, via_mqtt, 8) \ X(a, STATIC, OPTIONAL, UINT32, hops_away, 9) \ X(a, STATIC, SINGULAR, BOOL, is_favorite, 10) \ X(a, STATIC, SINGULAR, BOOL, is_ignored, 11) \ -X(a, STATIC, SINGULAR, BOOL, is_key_manually_verified, 12) +X(a, STATIC, SINGULAR, BOOL, is_key_manually_verified, 12) \ +X(a, STATIC, SINGULAR, BOOL, is_muted, 13) #define meshtastic_NodeInfo_CALLBACK NULL #define meshtastic_NodeInfo_DEFAULT NULL #define meshtastic_NodeInfo_user_MSGTYPE meshtastic_User @@ -1980,7 +2084,9 @@ 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_StoreForwardPlusPlus_msg; extern const pb_msgdesc_t meshtastic_Waypoint_msg; +extern const pb_msgdesc_t meshtastic_StatusMessage_msg; extern const pb_msgdesc_t meshtastic_MqttClientProxyMessage_msg; extern const pb_msgdesc_t meshtastic_MeshPacket_msg; extern const pb_msgdesc_t meshtastic_NodeInfo_msg; @@ -2013,7 +2119,9 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_Routing_fields &meshtastic_Routing_msg #define meshtastic_Data_fields &meshtastic_Data_msg #define meshtastic_KeyVerification_fields &meshtastic_KeyVerification_msg +#define meshtastic_StoreForwardPlusPlus_fields &meshtastic_StoreForwardPlusPlus_msg #define meshtastic_Waypoint_fields &meshtastic_Waypoint_msg +#define meshtastic_StatusMessage_fields &meshtastic_StatusMessage_msg #define meshtastic_MqttClientProxyMessage_fields &meshtastic_MqttClientProxyMessage_msg #define meshtastic_MeshPacket_fields &meshtastic_MeshPacket_msg #define meshtastic_NodeInfo_fields &meshtastic_NodeInfo_msg @@ -2063,12 +2171,14 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_MyNodeInfo_size 83 #define meshtastic_NeighborInfo_size 258 #define meshtastic_Neighbor_size 22 -#define meshtastic_NodeInfo_size 323 +#define meshtastic_NodeInfo_size 325 #define meshtastic_NodeRemoteHardwarePin_size 29 #define meshtastic_Position_size 144 #define meshtastic_QueueStatus_size 23 #define meshtastic_RouteDiscovery_size 256 #define meshtastic_Routing_size 259 +#define meshtastic_StatusMessage_size 81 +#define meshtastic_StoreForwardPlusPlus_size 377 #define meshtastic_ToRadio_size 504 #define meshtastic_User_size 115 #define meshtastic_Waypoint_size 165 diff --git a/src/mesh/generated/meshtastic/module_config.pb.cpp b/src/mesh/generated/meshtastic/module_config.pb.cpp index f262df6a3..bb57c3f2d 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.cpp +++ b/src/mesh/generated/meshtastic/module_config.pb.cpp @@ -51,6 +51,9 @@ PB_BIND(meshtastic_ModuleConfig_CannedMessageConfig, meshtastic_ModuleConfig_Can PB_BIND(meshtastic_ModuleConfig_AmbientLightingConfig, meshtastic_ModuleConfig_AmbientLightingConfig, AUTO) +PB_BIND(meshtastic_ModuleConfig_StatusMessageConfig, meshtastic_ModuleConfig_StatusMessageConfig, AUTO) + + PB_BIND(meshtastic_RemoteHardwarePin, meshtastic_RemoteHardwarePin, AUTO) diff --git a/src/mesh/generated/meshtastic/module_config.pb.h b/src/mesh/generated/meshtastic/module_config.pb.h index 47d3b5baa..46a7164d2 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.h +++ b/src/mesh/generated/meshtastic/module_config.pb.h @@ -84,8 +84,11 @@ typedef enum _meshtastic_ModuleConfig_SerialConfig_Serial_Mode { https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable */ meshtastic_ModuleConfig_SerialConfig_Serial_Mode_VE_DIRECT = 7, /* Used to configure and view some parameters of MeshSolar. -https://heltec.org/project/meshsolar/ */ - meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG = 8 + https://heltec.org/project/meshsolar/ */ + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG = 8, + /* Logs mesh traffic to the serial pins, ideal for logging via openLog or similar. */ + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_LOG = 9, /* includes other packets */ + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_LOGTEXT = 10 /* only text (channel & DM) */ } meshtastic_ModuleConfig_SerialConfig_Serial_Mode; /* TODO: REPLACE */ @@ -359,6 +362,8 @@ typedef struct _meshtastic_ModuleConfig_TelemetryConfig { /* Enable/Disable the device telemetry module to send metrics to the mesh Note: We will still send telemtry to the connected phone / client every minute over the API */ bool device_telemetry_enabled; + /* Enable/Disable the air quality telemetry measurement module on-device display */ + bool air_quality_screen_enabled; } meshtastic_ModuleConfig_TelemetryConfig; /* Canned Messages Module Config */ @@ -404,6 +409,12 @@ typedef struct _meshtastic_ModuleConfig_AmbientLightingConfig { uint8_t blue; } meshtastic_ModuleConfig_AmbientLightingConfig; +/* StatusMessage config - Allows setting a status message for a node to periodically rebroadcast */ +typedef struct _meshtastic_ModuleConfig_StatusMessageConfig { + /* The actual status string */ + char node_status[80]; +} meshtastic_ModuleConfig_StatusMessageConfig; + /* A GPIO pin definition for remote hardware module */ typedef struct _meshtastic_RemoteHardwarePin { /* GPIO Pin number (must match Arduino) */ @@ -455,6 +466,8 @@ typedef struct _meshtastic_ModuleConfig { meshtastic_ModuleConfig_DetectionSensorConfig detection_sensor; /* TODO: REPLACE */ meshtastic_ModuleConfig_PaxcounterConfig paxcounter; + /* TODO: REPLACE */ + meshtastic_ModuleConfig_StatusMessageConfig statusmessage; } payload_variant; } meshtastic_ModuleConfig; @@ -481,8 +494,8 @@ extern "C" { #define _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_ARRAYSIZE ((meshtastic_ModuleConfig_SerialConfig_Serial_Baud)(meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_921600+1)) #define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT -#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MAX meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG -#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_ARRAYSIZE ((meshtastic_ModuleConfig_SerialConfig_Serial_Mode)(meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG+1)) +#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MAX meshtastic_ModuleConfig_SerialConfig_Serial_Mode_LOGTEXT +#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_ARRAYSIZE ((meshtastic_ModuleConfig_SerialConfig_Serial_Mode)(meshtastic_ModuleConfig_SerialConfig_Serial_Mode_LOGTEXT+1)) #define _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE #define _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MAX meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK @@ -510,6 +523,7 @@ extern "C" { #define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_event_press_ENUMTYPE meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar + #define meshtastic_RemoteHardwarePin_type_ENUMTYPE meshtastic_RemoteHardwarePinType @@ -526,9 +540,10 @@ extern "C" { #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0, 0} -#define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_StatusMessageConfig_init_default {""} #define meshtastic_RemoteHardwarePin_init_default {0, "", _meshtastic_RemoteHardwarePinType_MIN} #define meshtastic_ModuleConfig_init_zero {0, {meshtastic_ModuleConfig_MQTTConfig_init_zero}} #define meshtastic_ModuleConfig_MQTTConfig_init_zero {0, "", "", "", 0, 0, 0, "", 0, 0, false, meshtastic_ModuleConfig_MapReportSettings_init_zero} @@ -542,9 +557,10 @@ extern "C" { #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0, 0} -#define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_StatusMessageConfig_init_zero {""} #define meshtastic_RemoteHardwarePin_init_zero {0, "", _meshtastic_RemoteHardwarePinType_MIN} /* Field tags (for use in manual encoding/decoding) */ @@ -631,6 +647,7 @@ extern "C" { #define meshtastic_ModuleConfig_TelemetryConfig_health_update_interval_tag 12 #define meshtastic_ModuleConfig_TelemetryConfig_health_screen_enabled_tag 13 #define meshtastic_ModuleConfig_TelemetryConfig_device_telemetry_enabled_tag 14 +#define meshtastic_ModuleConfig_TelemetryConfig_air_quality_screen_enabled_tag 15 #define meshtastic_ModuleConfig_CannedMessageConfig_rotary1_enabled_tag 1 #define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_a_tag 2 #define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_b_tag 3 @@ -647,6 +664,7 @@ extern "C" { #define meshtastic_ModuleConfig_AmbientLightingConfig_red_tag 3 #define meshtastic_ModuleConfig_AmbientLightingConfig_green_tag 4 #define meshtastic_ModuleConfig_AmbientLightingConfig_blue_tag 5 +#define meshtastic_ModuleConfig_StatusMessageConfig_node_status_tag 1 #define meshtastic_RemoteHardwarePin_gpio_pin_tag 1 #define meshtastic_RemoteHardwarePin_name_tag 2 #define meshtastic_RemoteHardwarePin_type_tag 3 @@ -666,6 +684,7 @@ extern "C" { #define meshtastic_ModuleConfig_ambient_lighting_tag 11 #define meshtastic_ModuleConfig_detection_sensor_tag 12 #define meshtastic_ModuleConfig_paxcounter_tag 13 +#define meshtastic_ModuleConfig_statusmessage_tag 14 /* Struct field encoding specification for nanopb */ #define meshtastic_ModuleConfig_FIELDLIST(X, a) \ @@ -681,7 +700,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,remote_hardware,payload_vari X(a, STATIC, ONEOF, MESSAGE, (payload_variant,neighbor_info,payload_variant.neighbor_info), 10) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ambient_lighting,payload_variant.ambient_lighting), 11) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,detection_sensor,payload_variant.detection_sensor), 12) \ -X(a, STATIC, ONEOF, MESSAGE, (payload_variant,paxcounter,payload_variant.paxcounter), 13) +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,paxcounter,payload_variant.paxcounter), 13) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,statusmessage,payload_variant.statusmessage), 14) #define meshtastic_ModuleConfig_CALLBACK NULL #define meshtastic_ModuleConfig_DEFAULT NULL #define meshtastic_ModuleConfig_payload_variant_mqtt_MSGTYPE meshtastic_ModuleConfig_MQTTConfig @@ -697,6 +717,7 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,paxcounter,payload_variant.p #define meshtastic_ModuleConfig_payload_variant_ambient_lighting_MSGTYPE meshtastic_ModuleConfig_AmbientLightingConfig #define meshtastic_ModuleConfig_payload_variant_detection_sensor_MSGTYPE meshtastic_ModuleConfig_DetectionSensorConfig #define meshtastic_ModuleConfig_payload_variant_paxcounter_MSGTYPE meshtastic_ModuleConfig_PaxcounterConfig +#define meshtastic_ModuleConfig_payload_variant_statusmessage_MSGTYPE meshtastic_ModuleConfig_StatusMessageConfig #define meshtastic_ModuleConfig_MQTTConfig_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ @@ -830,7 +851,8 @@ X(a, STATIC, SINGULAR, BOOL, power_screen_enabled, 10) \ X(a, STATIC, SINGULAR, BOOL, health_measurement_enabled, 11) \ X(a, STATIC, SINGULAR, UINT32, health_update_interval, 12) \ X(a, STATIC, SINGULAR, BOOL, health_screen_enabled, 13) \ -X(a, STATIC, SINGULAR, BOOL, device_telemetry_enabled, 14) +X(a, STATIC, SINGULAR, BOOL, device_telemetry_enabled, 14) \ +X(a, STATIC, SINGULAR, BOOL, air_quality_screen_enabled, 15) #define meshtastic_ModuleConfig_TelemetryConfig_CALLBACK NULL #define meshtastic_ModuleConfig_TelemetryConfig_DEFAULT NULL @@ -858,6 +880,11 @@ X(a, STATIC, SINGULAR, UINT32, blue, 5) #define meshtastic_ModuleConfig_AmbientLightingConfig_CALLBACK NULL #define meshtastic_ModuleConfig_AmbientLightingConfig_DEFAULT NULL +#define meshtastic_ModuleConfig_StatusMessageConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, node_status, 1) +#define meshtastic_ModuleConfig_StatusMessageConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_StatusMessageConfig_DEFAULT NULL + #define meshtastic_RemoteHardwarePin_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, gpio_pin, 1) \ X(a, STATIC, SINGULAR, STRING, name, 2) \ @@ -880,6 +907,7 @@ extern const pb_msgdesc_t meshtastic_ModuleConfig_RangeTestConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_TelemetryConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_CannedMessageConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_AmbientLightingConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_StatusMessageConfig_msg; extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ @@ -898,6 +926,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_TelemetryConfig_fields &meshtastic_ModuleConfig_TelemetryConfig_msg #define meshtastic_ModuleConfig_CannedMessageConfig_fields &meshtastic_ModuleConfig_CannedMessageConfig_msg #define meshtastic_ModuleConfig_AmbientLightingConfig_fields &meshtastic_ModuleConfig_AmbientLightingConfig_msg +#define meshtastic_ModuleConfig_StatusMessageConfig_fields &meshtastic_ModuleConfig_StatusMessageConfig_msg #define meshtastic_RemoteHardwarePin_fields &meshtastic_RemoteHardwarePin_msg /* Maximum encoded size of messages (where known) */ @@ -914,8 +943,9 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_RangeTestConfig_size 12 #define meshtastic_ModuleConfig_RemoteHardwareConfig_size 96 #define meshtastic_ModuleConfig_SerialConfig_size 28 +#define meshtastic_ModuleConfig_StatusMessageConfig_size 81 #define meshtastic_ModuleConfig_StoreForwardConfig_size 24 -#define meshtastic_ModuleConfig_TelemetryConfig_size 48 +#define meshtastic_ModuleConfig_TelemetryConfig_size 50 #define meshtastic_ModuleConfig_size 227 #define meshtastic_RemoteHardwarePin_size 21 diff --git a/src/mesh/generated/meshtastic/portnums.pb.h b/src/mesh/generated/meshtastic/portnums.pb.h index 67adc60cc..d31daa4b2 100644 --- a/src/mesh/generated/meshtastic/portnums.pb.h +++ b/src/mesh/generated/meshtastic/portnums.pb.h @@ -86,6 +86,16 @@ typedef enum _meshtastic_PortNum { /* Paxcounter lib included in the firmware ENCODING: protobuf */ meshtastic_PortNum_PAXCOUNTER_APP = 34, + /* Store and Forward++ module included in the firmware + ENCODING: protobuf + This module is specifically for Native Linux nodes, and provides a Git-style + chain of messages. */ + meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP = 35, + /* Node Status module + ENCODING: protobuf + This module allows setting an extra string of status for a node. + Broadcasts on change and on a timer, possibly once a day. */ + meshtastic_PortNum_NODE_STATUS_APP = 36, /* Provides a hardware serial interface to send and receive from the Meshtastic network. Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic network is forwarded to the RX pin while sending a packet to TX will go out to the Mesh network. diff --git a/src/mesh/generated/meshtastic/telemetry.pb.cpp b/src/mesh/generated/meshtastic/telemetry.pb.cpp index 345d7a157..fff75ebc1 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.cpp +++ b/src/mesh/generated/meshtastic/telemetry.pb.cpp @@ -33,6 +33,9 @@ PB_BIND(meshtastic_Telemetry, meshtastic_Telemetry, 2) PB_BIND(meshtastic_Nau7802Config, meshtastic_Nau7802Config, AUTO) +PB_BIND(meshtastic_SEN5XState, meshtastic_SEN5XState, AUTO) + + diff --git a/src/mesh/generated/meshtastic/telemetry.pb.h b/src/mesh/generated/meshtastic/telemetry.pb.h index dec89ba15..dc9d876dc 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.h +++ b/src/mesh/generated/meshtastic/telemetry.pb.h @@ -361,6 +361,8 @@ typedef struct _meshtastic_LocalStats { uint32_t heap_free_bytes; /* Number of packets that were dropped because the transmit queue was full. */ uint16_t num_tx_dropped; + /* Noise floor value measured in dBm */ + int32_t noise_floor; } meshtastic_LocalStats; /* Health telemetry metrics */ @@ -433,6 +435,25 @@ typedef struct _meshtastic_Nau7802Config { float calibrationFactor; } meshtastic_Nau7802Config; +/* SEN5X State, for saving to flash */ +typedef struct _meshtastic_SEN5XState { + /* Last cleaning time for SEN5X */ + uint32_t last_cleaning_time; + /* Last cleaning time for SEN5X - valid flag */ + bool last_cleaning_valid; + /* Config flag for one-shot mode (see admin.proto) */ + bool one_shot_mode; + /* Last VOC state time for SEN55 */ + bool has_voc_state_time; + uint32_t voc_state_time; + /* Last VOC state validity flag for SEN55 */ + bool has_voc_state_valid; + bool voc_state_valid; + /* VOC state array (8x uint8t) for SEN55 */ + bool has_voc_state_array; + uint64_t voc_state_array; +} meshtastic_SEN5XState; + #ifdef __cplusplus extern "C" { @@ -453,25 +474,28 @@ extern "C" { + /* Initializer values for message structs */ #define meshtastic_DeviceMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0} #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, false, 0, false, 0, false, 0, false, 0, 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, 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, 0, 0, 0} +#define meshtastic_LocalStats_init_default {0, 0, 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}} #define meshtastic_Nau7802Config_init_default {0, 0} +#define meshtastic_SEN5XState_init_default {0, 0, 0, false, 0, false, 0, false, 0} #define meshtastic_DeviceMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0} #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, false, 0, false, 0, false, 0, false, 0, 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, 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, 0, 0, 0} +#define meshtastic_LocalStats_init_zero {0, 0, 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}} #define meshtastic_Nau7802Config_init_zero {0, 0} +#define meshtastic_SEN5XState_init_zero {0, 0, 0, false, 0, false, 0, false, 0} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_DeviceMetrics_battery_level_tag 1 @@ -556,6 +580,7 @@ extern "C" { #define meshtastic_LocalStats_heap_total_bytes_tag 12 #define meshtastic_LocalStats_heap_free_bytes_tag 13 #define meshtastic_LocalStats_num_tx_dropped_tag 14 +#define meshtastic_LocalStats_noise_floor_tag 15 #define meshtastic_HealthMetrics_heart_bpm_tag 1 #define meshtastic_HealthMetrics_spO2_tag 2 #define meshtastic_HealthMetrics_temperature_tag 3 @@ -578,6 +603,12 @@ extern "C" { #define meshtastic_Telemetry_host_metrics_tag 8 #define meshtastic_Nau7802Config_zeroOffset_tag 1 #define meshtastic_Nau7802Config_calibrationFactor_tag 2 +#define meshtastic_SEN5XState_last_cleaning_time_tag 1 +#define meshtastic_SEN5XState_last_cleaning_valid_tag 2 +#define meshtastic_SEN5XState_one_shot_mode_tag 3 +#define meshtastic_SEN5XState_voc_state_time_tag 4 +#define meshtastic_SEN5XState_voc_state_valid_tag 5 +#define meshtastic_SEN5XState_voc_state_array_tag 6 /* Struct field encoding specification for nanopb */ #define meshtastic_DeviceMetrics_FIELDLIST(X, a) \ @@ -678,7 +709,8 @@ X(a, STATIC, SINGULAR, UINT32, num_tx_relay, 10) \ 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) \ -X(a, STATIC, SINGULAR, UINT32, num_tx_dropped, 14) +X(a, STATIC, SINGULAR, UINT32, num_tx_dropped, 14) \ +X(a, STATIC, SINGULAR, INT32, noise_floor, 15) #define meshtastic_LocalStats_CALLBACK NULL #define meshtastic_LocalStats_DEFAULT NULL @@ -727,6 +759,16 @@ X(a, STATIC, SINGULAR, FLOAT, calibrationFactor, 2) #define meshtastic_Nau7802Config_CALLBACK NULL #define meshtastic_Nau7802Config_DEFAULT NULL +#define meshtastic_SEN5XState_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, last_cleaning_time, 1) \ +X(a, STATIC, SINGULAR, BOOL, last_cleaning_valid, 2) \ +X(a, STATIC, SINGULAR, BOOL, one_shot_mode, 3) \ +X(a, STATIC, OPTIONAL, UINT32, voc_state_time, 4) \ +X(a, STATIC, OPTIONAL, BOOL, voc_state_valid, 5) \ +X(a, STATIC, OPTIONAL, FIXED64, voc_state_array, 6) +#define meshtastic_SEN5XState_CALLBACK NULL +#define meshtastic_SEN5XState_DEFAULT NULL + extern const pb_msgdesc_t meshtastic_DeviceMetrics_msg; extern const pb_msgdesc_t meshtastic_EnvironmentMetrics_msg; extern const pb_msgdesc_t meshtastic_PowerMetrics_msg; @@ -736,6 +778,7 @@ extern const pb_msgdesc_t meshtastic_HealthMetrics_msg; extern const pb_msgdesc_t meshtastic_HostMetrics_msg; extern const pb_msgdesc_t meshtastic_Telemetry_msg; extern const pb_msgdesc_t meshtastic_Nau7802Config_msg; +extern const pb_msgdesc_t meshtastic_SEN5XState_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_DeviceMetrics_fields &meshtastic_DeviceMetrics_msg @@ -747,6 +790,7 @@ extern const pb_msgdesc_t meshtastic_Nau7802Config_msg; #define meshtastic_HostMetrics_fields &meshtastic_HostMetrics_msg #define meshtastic_Telemetry_fields &meshtastic_Telemetry_msg #define meshtastic_Nau7802Config_fields &meshtastic_Nau7802Config_msg +#define meshtastic_SEN5XState_fields &meshtastic_SEN5XState_msg /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_TELEMETRY_PB_H_MAX_SIZE meshtastic_Telemetry_size @@ -755,9 +799,10 @@ 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 76 +#define meshtastic_LocalStats_size 87 #define meshtastic_Nau7802Config_size 16 #define meshtastic_PowerMetrics_size 81 +#define meshtastic_SEN5XState_size 27 #define meshtastic_Telemetry_size 272 #ifdef __cplusplus diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 7b7ebb595..ea8d6af8e 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -173,7 +173,7 @@ void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res) if (req->getMethod() == "OPTIONS") { res->setStatusCode(204); // Success with no content - // res->print(""); @todo remove + res->print(""); return; } @@ -223,7 +223,7 @@ void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res) if (req->getMethod() == "OPTIONS") { res->setStatusCode(204); // Success with no content - // res->print(""); @todo remove + res->print(""); return; } diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp index 45944872e..a95dfa58f 100644 --- a/src/mesh/wifi/WiFiAPClient.cpp +++ b/src/mesh/wifi/WiFiAPClient.cpp @@ -58,7 +58,7 @@ bool needReconnect = true; // If we create our reconnector, run it once at the bool isReconnecting = false; // If we are currently reconnecting WiFiUDP syslogClient; -Syslog syslog(syslogClient); +meshtastic::Syslog syslog(syslogClient); Periodic *wifiReconnect; diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index aa510a86d..61dbc0b92 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -9,11 +9,8 @@ #include "meshUtils.h" #include #include // for better whitespace handling -#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH -#include "BleOta.h" -#endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI -#include "WiFiOTA.h" +#include "MeshtasticOTA.h" #endif #include "Router.h" #include "configuration.h" @@ -236,26 +233,51 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta reboot(r->reboot_seconds); break; } - case meshtastic_AdminMessage_reboot_ota_seconds_tag: { - int32_t s = r->reboot_ota_seconds; + case meshtastic_AdminMessage_ota_request_tag: { #if defined(ARCH_ESP32) -#if !MESHTASTIC_EXCLUDE_BLUETOOTH - if (!BleOta::getOtaAppVersion().isEmpty()) { + LOG_INFO("OTA Requested"); + + if (r->ota_request.ota_hash.size != 32) { + suppressRebootBanner = true; + sendWarningAndLog("Cannot start OTA: Invalid `ota_hash` provided."); + break; + } + + meshtastic_OTAMode mode = r->ota_request.reboot_ota_mode; + const char *mode_name = (mode == METHOD_OTA_BLE ? "BLE" : "WiFi"); + + // Check that we have an OTA partition + const esp_partition_t *part = MeshtasticOTA::getAppPartition(); + if (part == NULL) { + suppressRebootBanner = true; + sendWarningAndLog("Cannot start OTA: Cannot find OTA Loader partition."); + break; + } + + static esp_app_desc_t app_desc; + if (!MeshtasticOTA::getAppDesc(part, &app_desc)) { + suppressRebootBanner = true; + sendWarningAndLog("Cannot start OTA: Device does have a valid OTA Loader."); + break; + } + + if (!MeshtasticOTA::checkOTACapability(&app_desc, mode)) { + suppressRebootBanner = true; + sendWarningAndLog("OTA Loader does not support %s", mode_name); + break; + } + + if (MeshtasticOTA::trySwitchToOTA()) { + suppressRebootBanner = true; if (screen) screen->startFirmwareUpdateScreen(); - BleOta::switchToOtaApp(); - LOG_INFO("Rebooting to BLE OTA"); + MeshtasticOTA::saveConfig(&config.network, mode, r->ota_request.ota_hash.bytes); + sendWarningAndLog("Rebooting to %s OTA", mode_name); + } else { + sendWarningAndLog("Unable to switch to the OTA partition."); } #endif -#if !MESHTASTIC_EXCLUDE_WIFI - if (WiFiOTA::trySwitchToOTA()) { - if (screen) - screen->startFirmwareUpdateScreen(); - WiFiOTA::saveConfig(&config.network); - LOG_INFO("Rebooting to WiFi OTA"); - } -#endif -#endif + int s = 1; // Reboot in 1 second, hard coded LOG_INFO("Reboot in %d seconds", s); rebootAtMsec = (s < 0) ? 0 : (millis() + s * 1000); break; @@ -383,6 +405,16 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta } break; } + case meshtastic_AdminMessage_toggle_muted_node_tag: { + LOG_INFO("Client received toggle_muted_node command"); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->toggle_muted_node); + if (node != NULL) { + node->bitfield ^= (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT); + saveChanges(SEGMENT_NODEDATABASE, false); + } + break; + } + case meshtastic_AdminMessage_set_fixed_position_tag: { LOG_INFO("Client received set_fixed_position command"); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); @@ -417,6 +449,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta } case meshtastic_AdminMessage_enter_dfu_mode_request_tag: { LOG_INFO("Client requesting to enter DFU mode"); +#if HAS_SCREEN + IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0)); +#endif #if defined(ARCH_NRF52) || defined(ARCH_RP2040) enterDfuMode(); #endif @@ -870,10 +905,11 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) { + bool shouldReboot = true; // If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth // Otherwise, disable Bluetooth to prevent the phone from interfering with the config - if (!hasOpenEditTransaction && - !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) { + if (!hasOpenEditTransaction && !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, + meshtastic_ModuleConfig_serial_tag, meshtastic_ModuleConfig_statusmessage_tag)) { disableBluetooth(); } @@ -965,8 +1001,14 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) moduleConfig.has_paxcounter = true; moduleConfig.paxcounter = c.payload_variant.paxcounter; break; + case meshtastic_ModuleConfig_statusmessage_tag: + LOG_INFO("Set module config: StatusMessage"); + moduleConfig.has_statusmessage = true; + moduleConfig.statusmessage = c.payload_variant.statusmessage; + shouldReboot = false; + break; } - saveChanges(SEGMENT_MODULECONFIG); + saveChanges(SEGMENT_MODULECONFIG, shouldReboot); return true; } @@ -1145,6 +1187,11 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter; break; + case meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG: + LOG_INFO("Get module config: StatusMessage"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_statusmessage_tag; + res.get_module_config_response.payload_variant.statusmessage = moduleConfig.statusmessage; + break; } // NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior. @@ -1461,15 +1508,43 @@ void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent #endif } -void AdminModule::sendWarning(const char *message) +void AdminModule::sendWarning(const char *format, ...) { meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + if (!cn) + return; + cn->level = meshtastic_LogRecord_Level_WARNING; cn->time = getValidTime(RTCQualityFromNet); - strncpy(cn->message, message, sizeof(cn->message)); + + va_list args; + va_start(args, format); + // Format the arguments directly into the notification object + vsnprintf(cn->message, sizeof(cn->message), format, args); + va_end(args); + service->sendClientNotification(cn); } +void AdminModule::sendWarningAndLog(const char *format, ...) +{ + // We need a temporary buffer to hold the formatted text so we can log it + // Using 250 bytes as a safe upper limit for typical text notifications + char buf[250]; + + va_list args; + va_start(args, format); + vsnprintf(buf, sizeof(buf), format, args); + va_end(args); + + LOG_WARN(buf); + // 2. Call sendWarning + // SECURITY NOTE: We pass "%s", buf instead of just 'buf'. + // If 'buf' contained a % symbol (e.g. "Battery 50%"), passing it directly + // would crash sendWarning. "%s" treats it purely as text. + sendWarning("%s", buf); +} + void disableBluetooth() { #if HAS_BLUETOOTH diff --git a/src/modules/AdminModule.h b/src/modules/AdminModule.h index 867751f49..c446887b3 100644 --- a/src/modules/AdminModule.h +++ b/src/modules/AdminModule.h @@ -1,7 +1,9 @@ -#include - #pragma once +#ifdef ESP_PLATFORM +#include +#endif #include "ProtobufModule.h" +#include #if HAS_WIFI #include "mesh/wifi/WiFiAPClient.h" #endif @@ -71,7 +73,8 @@ class AdminModule : public ProtobufModule, public Obser bool messageIsResponse(const meshtastic_AdminMessage *r); bool messageIsRequest(const meshtastic_AdminMessage *r); - void sendWarning(const char *message); + void sendWarning(const char *format, ...) __attribute__((format(printf, 2, 3))); + void sendWarningAndLog(const char *format, ...) __attribute__((format(printf, 2, 3))); }; static constexpr const char *licensedModeMessage = diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 73ee26903..7d7b3cdb1 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -7,12 +7,15 @@ #include "Channels.h" #include "FSCommon.h" #include "MeshService.h" +#include "MessageStore.h" #include "NodeDB.h" #include "SPILock.h" #include "buzz.h" #include "detect/ScanI2C.h" +#include "gps/RTC.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/draw/MessageRenderer.h" #include "graphics/draw/NotificationRenderer.h" #include "graphics/emotes.h" #include "graphics/images.h" @@ -21,6 +24,7 @@ #include "mesh/generated/meshtastic/cannedmessages.pb.h" #include "modules/AdminModule.h" #include "modules/ExternalNotificationModule.h" // for buzzer control +extern MessageStore messageStore; #if HAS_TRACKBALL #include "input/TrackballInterruptImpl1.h" #endif @@ -41,8 +45,76 @@ // Remove Canned message screen if no action is taken for some milliseconds #define INACTIVATE_AFTER_MS 20000 +// Tokenize a message string into emote/text segments +static std::vector> tokenizeMessageWithEmotes(const char *msg) +{ + std::vector> tokens; + 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; + } + } + } + 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; + } + } + } + return tokens; +} + +// Render a single emote token centered vertically on a row +static void renderEmote(OLEDDisplay *display, int &nextX, int lineY, int rowHeight, const String &label) +{ + const graphics::Emote *emote = nullptr; + for (int j = 0; j < graphics::numEmotes; j++) { + if (label == graphics::emotes[j].label) { + emote = &graphics::emotes[j]; + break; + } + } + if (emote) { + int emoteYOffset = (rowHeight - emote->height) / 2; // vertically center the emote + display->drawXbm(nextX, lineY + emoteYOffset, emote->width, emote->height, emote->bitmap); + nextX += emote->width + 2; // spacing between tokens + } +} + +namespace graphics +{ +extern int bannerSignalBars; +} extern ScanI2C::DeviceAddress cardkb_found; -extern bool graphics::isMuted; extern bool osk_found; static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; @@ -58,8 +130,7 @@ CannedMessageModule::CannedMessageModule() : SinglePortModule("canned", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("CannedMessage") { this->loadProtoForModule(); - if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE && - !CANNED_MESSAGE_MODULE_ENABLE) { + if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE) { LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled"); this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED; disable(); @@ -72,18 +143,16 @@ CannedMessageModule::CannedMessageModule() void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChannel) { - // Use the requested destination, unless it's "broadcast" and we have a previous node/channel - if (newDest == NODENUM_BROADCAST && lastDestSet) { - newDest = lastDest; - newChannel = lastChannel; - } + // Do NOT override explicit broadcast replies + // Only reuse lastDest in LaunchRepeatDestination() + dest = newDest; channel = newChannel; + lastDest = dest; lastChannel = channel; lastDestSet = true; - // Rest of function unchanged... // Upon activation, highlight "[Select Destination]" int selectDestination = 0; for (int i = 0; i < messagesCount; ++i) { @@ -100,6 +169,8 @@ void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChan UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); + + LOG_DEBUG("[CannedMessage] LaunchWithDestination dest=0x%08x ch=%d", dest, channel); } void CannedMessageModule::LaunchRepeatDestination() @@ -113,13 +184,12 @@ void CannedMessageModule::LaunchRepeatDestination() void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t newChannel) { - // Use the requested destination, unless it's "broadcast" and we have a previous node/channel - if (newDest == NODENUM_BROADCAST && lastDestSet) { - newDest = lastDest; - newChannel = lastChannel; - } + // Do NOT override explicit broadcast replies + // Only reuse lastDest in LaunchRepeatDestination() + dest = newDest; channel = newChannel; + lastDest = dest; lastChannel = channel; lastDestSet = true; @@ -129,6 +199,8 @@ void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); + + LOG_DEBUG("[CannedMessage] LaunchFreetextWithDestination dest=0x%08x ch=%d", dest, channel); } static bool returnToCannedList = false; @@ -150,7 +222,7 @@ int CannedMessageModule::splitConfiguredMessages() String canned_messages = cannedMessageModuleConfig.messages; // Copy all message parts into the buffer - strncpy(this->messageStore, canned_messages.c_str(), sizeof(this->messageStore)); + strncpy(this->messageBuffer, canned_messages.c_str(), sizeof(this->messageBuffer)); // Temporary array to allow for insertion const char *tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0}; @@ -167,16 +239,16 @@ int CannedMessageModule::splitConfiguredMessages() #endif // First message always starts at buffer start - tempMessages[tempCount++] = this->messageStore; - int upTo = strlen(this->messageStore) - 1; + tempMessages[tempCount++] = this->messageBuffer; + int upTo = strlen(this->messageBuffer) - 1; // Walk buffer, splitting on '|' while (i < upTo) { - if (this->messageStore[i] == '|') { - this->messageStore[i] = '\0'; // End previous message + if (this->messageBuffer[i] == '|') { + this->messageBuffer[i] = '\0'; // End previous message if (tempCount >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) break; - tempMessages[tempCount++] = (this->messageStore + i + 1); + tempMessages[tempCount++] = (this->messageBuffer + i + 1); } i += 1; } @@ -194,25 +266,23 @@ int CannedMessageModule::splitConfiguredMessages() } void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer) { - if (graphics::isHighResolution) { + if (graphics::currentResolution == graphics::ScreenResolution::High) { if (this->dest == NODENUM_BROADCAST) { - display->drawStringf(x, y, buffer, "To: Broadcast@%s", channels.getName(this->channel)); + display->drawStringf(x, y, buffer, "To: #%s", channels.getName(this->channel)); } else { - display->drawStringf(x, y, buffer, "To: %s", getNodeName(this->dest)); + 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)); + display->drawStringf(x, y, buffer, "To: #%.20s", channels.getName(this->channel)); } else { - display->drawStringf(x, y, buffer, "To: %s", getNodeName(this->dest)); + 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 = ""; @@ -274,6 +344,10 @@ void CannedMessageModule::updateDestinationSelectionList() } } + meshtastic_MeshPacket *p = allocDataPacket(); + p->pki_encrypted = true; + p->channel = 0; + // Populate active channels std::vector seenChannels; seenChannels.reserve(channels.getNumChannels()); @@ -285,15 +359,6 @@ void CannedMessageModule::updateDestinationSelectionList() } } - /* As the nodeDB is sorted, can skip this step - // 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) { @@ -361,16 +426,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) 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) { + if (event->inputEvent == INPUT_BROKER_ALT_LONG) { LaunchWithDestination(NODENUM_BROADCAST); return 1; } @@ -384,6 +440,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) // Immediately process the input in the new state (freetext) return handleFreeTextInput(event); } + return 0; break; // (Other states can be added here as needed) @@ -574,7 +631,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) return false; - // === Handle Cancel key: go inactive, clear UI state === + // 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; @@ -603,7 +660,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo } else if (isSelect) { const char *current = messages[currentMessageIndex]; - // === [Select Destination] triggers destination selection UI === + // [Select Destination] triggers destination selection UI if (strcmp(current, "[Select Destination]") == 0) { returnToCannedList = true; runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; @@ -614,7 +671,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo return true; } - // === [Exit] returns to the main/inactive screen === + // [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; @@ -628,7 +685,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo return true; } - // === [Free Text] triggers the free text input (virtual keyboard) === + // [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; @@ -643,9 +700,9 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo if (osk_found && screen) { char headerBuffer[64]; if (this->dest == NODENUM_BROADCAST) { - snprintf(headerBuffer, sizeof(headerBuffer), "To: Broadcast@%s", channels.getName(this->channel)); + snprintf(headerBuffer, sizeof(headerBuffer), "To: #%s", channels.getName(this->channel)); } else { - snprintf(headerBuffer, sizeof(headerBuffer), "To: %s", getNodeName(this->dest)); + snprintf(headerBuffer, sizeof(headerBuffer), "To: @%s", getNodeName(this->dest)); } screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) { if (!text.empty()) { @@ -694,20 +751,12 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { } else { #if CANNED_MESSAGE_ADD_CONFIRMATION - // Show confirmation dialog before sending canned message - NodeNum destNode = dest; - ChannelIndex chan = channel; - graphics::menuHandler::showConfirmationBanner("Send message?", [this, destNode, chan, current]() { - this->sendText(destNode, chan, current, false); - payload = runState; - 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(); + const int savedIndex = currentMessageIndex; + graphics::menuHandler::showConfirmationBanner("Send message?", [this, savedIndex]() { + this->currentMessageIndex = savedIndex; + this->payload = this->runState; + this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + this->setIntervalFromNow(0); }); #else payload = runState; @@ -806,7 +855,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) } #endif // USE_VIRTUAL_KEYBOARD - // ---- All hardware keys fall through to here (CardKB, physical, etc.) ---- + // 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; @@ -950,57 +999,110 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha lastDest = dest; lastChannel = channel; lastDestSet = true; - // === Prepare packet === + meshtastic_MeshPacket *p = allocDataPacket(); p->to = dest; p->channel = channel; p->want_ack = true; + p->decoded.dest = dest; // Mirror picker: NODENUM_BROADCAST or node->num - // Save destination for ACK/NACK UI fallback this->lastSentNode = dest; this->incoming = dest; - // Copy message payload + // Manually find the node by number to check PKI capability + meshtastic_NodeInfoLite *node = nullptr; + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + for (size_t i = 0; i < numMeshNodes; ++i) { + meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (n && n->num == dest) { + node = n; + break; + } + } + + NodeNum myNodeNum = nodeDB->getNodeNum(); + if (node && node->num != myNodeNum && node->has_user && node->user.public_key.size == 32) { + p->pki_encrypted = true; + p->channel = 0; // force PKI + } + + // Track this packet’s request ID for matching ACKs + this->lastRequestId = p->id; + + // Copy 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 - p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; // Null-terminate + p->decoded.payload.bytes[p->decoded.payload.size++] = 7; + p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; } - // Mark as waiting for ACK to trigger ACK/NACK screen this->waitingForAck = true; - // Log outgoing message - LOG_INFO("Send message id=%u, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); - - if (p->to != 0xffffffff) { - // Only add as favorite if our role is NOT CLIENT_BASE - if (config.device.role != 12) { - LOG_INFO("Proactively adding %x as favorite node", p->to); - nodeDB->set_favorite(true, p->to); - } else { - LOG_DEBUG("Not favoriting node %x as we are CLIENT_BASE role", p->to); - } - - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - p->pki_encrypted = true; - p->channel = 0; - } - - // Send to mesh and phone (even if no phone connected, to track ACKs) + // Send to mesh (PKI-encrypted if conditions above matched) service->sendToMesh(p, RX_SRC_LOCAL, true); - // === Simulate local message to clear unread UI === + // Show banner immediately if (screen) { - meshtastic_MeshPacket simulatedPacket = {}; - simulatedPacket.from = 0; // Local device - screen->handleTextMessage(&simulatedPacket); + graphics::BannerOverlayOptions opts; + opts.message = "Sending..."; + opts.durationMs = 2000; + screen->showOverlayBanner(opts); } + + // Save outgoing message + StoredMessage sm; + + // Always use our local time, consistent with other paths + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + sm.timestamp = (nowSecs > 0) ? nowSecs : millis() / 1000; + sm.isBootRelative = (nowSecs == 0); + + sm.sender = nodeDB->getNodeNum(); // us + sm.channelIndex = channel; + size_t len = strnlen(message, MAX_MESSAGE_SIZE - 1); + sm.textOffset = MessageStore::storeText(message, len); + sm.textLength = len; + + // Classify broadcast vs DM + if (dest == NODENUM_BROADCAST) { + sm.dest = NODENUM_BROADCAST; + sm.type = MessageType::BROADCAST; + } else { + sm.dest = dest; + sm.type = MessageType::DM_TO_US; + // Only add as favorite if our role is NOT CLIENT_BASE + if (config.device.role != 12) { + LOG_INFO("Proactively adding %x as favorite node", dest); + nodeDB->set_favorite(true, dest); + } else { + LOG_DEBUG("Not favoriting node %x as we are CLIENT_BASE role", dest); + } + } + sm.ackStatus = AckStatus::NONE; + + messageStore.addLiveMessage(std::move(sm)); + + // Auto-switch thread view on outgoing message + if (sm.type == MessageType::BROADCAST) { + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, sm.channelIndex); + } else { + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, sm.dest); + } + playComboTune(); + + this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + this->payload = wantReplies ? 1 : 0; + requestFocus(); + + // Tell Screen to switch to TextMessage frame via UIFrameEvent + UIFrameEvent e; + e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + notifyObservers(&e); } + int32_t CannedMessageModule::runOnce() { if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION && needsUpdate) { @@ -1021,7 +1123,6 @@ int32_t CannedMessageModule::runOnce() graphics::OnScreenKeyboardModule::instance().stop(false); } - temporaryMessage = ""; return INT32_MAX; } @@ -1063,7 +1164,6 @@ int32_t CannedMessageModule::runOnce() (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; this->currentMessageIndex = -1; this->freetext = ""; @@ -1072,15 +1172,11 @@ int32_t CannedMessageModule::runOnce() } // Handle SENDING_ACTIVE state transition after virtual keyboard message else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) { - // This happens after virtual keyboard message sending is complete - LOG_INFO("Virtual keyboard message sending completed, returning to inactive state"); this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - temporaryMessage = ""; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; - this->notifyObservers(&e); + return INT32_MAX; } 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 on inactivity @@ -1106,7 +1202,21 @@ int32_t CannedMessageModule::runOnce() } else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { sendText(this->dest, this->channel, this->freetext.c_str(), true); - this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + + // Clean up state but *don’t* deactivate yet + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + + // Tell Screen to jump straight to the TextMessage frame + UIFrameEvent e; + e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + this->notifyObservers(&e); + + // Now deactivate this module + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + + return INT32_MAX; // don’t fall back into canned list } else { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } @@ -1120,37 +1230,59 @@ int32_t CannedMessageModule::runOnce() return INT32_MAX; } else { sendText(this->dest, this->channel, this->messages[this->currentMessageIndex], true); + + // Clean up state + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + + // Tell Screen to jump straight to the TextMessage frame + UIFrameEvent e; + e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + this->notifyObservers(&e); + + // Now deactivate this module + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + + return INT32_MAX; // don’t fall back into canned list } - this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; } else { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } } + // fallback clean-up if nothing above returned this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->notifyObservers(&e); - return 2000; + + // Immediately stop, don’t linger on canned screen + return INT32_MAX; } // Highlight [Select Destination] initially when entering the message list else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { - int selectDestination = 0; - for (int i = 0; i < this->messagesCount; ++i) { - if (strcmp(this->messages[i], "[Select Destination]") == 0) { - selectDestination = i; - break; + // Only auto-highlight [Select Destination] if we’re ACTIVELY browsing, + // not when coming back from a sent message. + if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + int selectDestination = 0; + for (int i = 0; i < this->messagesCount; ++i) { + if (strcmp(this->messages[i], "[Select Destination]") == 0) { + selectDestination = i; + break; + } } + this->currentMessageIndex = selectDestination; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; } - this->currentMessageIndex = selectDestination; - 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 = ""; this->cursor = 0; 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) { @@ -1158,7 +1290,6 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; 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) { @@ -1208,7 +1339,7 @@ int32_t CannedMessageModule::runOnce() this->freetext.substring(this->cursor); } this->cursor++; - uint16_t maxChars = meshtastic_Constants_DATA_PAYLOAD_LEN - (moduleConfig.canned_message.send_bell ? 1 : 0); + const uint16_t maxChars = 200 - (moduleConfig.canned_message.send_bell ? 1 : 0); if (this->freetext.length() > maxChars) { this->cursor = maxChars; this->freetext = this->freetext.substring(0, maxChars); @@ -1264,7 +1395,10 @@ const char *CannedMessageModule::getNodeName(NodeNum node) bool CannedMessageModule::shouldDraw() { - return (currentMessageIndex != -1) || (this->runState != CANNED_MESSAGE_RUN_STATE_INACTIVE); + // Only allow drawing when we're in an interactive UI state. + return (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || + this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || + this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER); } // Has the user defined any canned messages? @@ -1291,16 +1425,6 @@ int CannedMessageModule::getPrevIndex() return this->currentMessageIndex - 1; } } -void CannedMessageModule::showTemporaryMessage(const String &message) -{ - temporaryMessage = 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_SELECTION; - // run this loop again in 2 seconds, next iteration will clear the display - setIntervalFromNow(2000); -} #if defined(USE_VIRTUAL_KEYBOARD) @@ -1523,7 +1647,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - // === Header === + // Header int titleY = 2; String titleText = "Select Destination"; titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]"; @@ -1531,7 +1655,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O display->drawString(display->getWidth() / 2, titleY, titleText); display->setTextAlignment(TEXT_ALIGN_LEFT); - // === List Items === + // List Items int rowYOffset = titleY + (FONT_HEIGHT_SMALL - 4); int numActiveChannels = this->activeChannelIndices.size(); int totalEntries = numActiveChannels + this->filteredNodes.size(); @@ -1540,7 +1664,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O if (this->visibleRows < 1) this->visibleRows = 1; - // === Clamp scrolling === + // Clamp scrolling if (scrollIndex > totalEntries / columns) scrollIndex = totalEntries / columns; if (scrollIndex < 0) @@ -1558,26 +1682,44 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O // Draw Channels First if (itemIndex < numActiveChannels) { uint8_t channelIndex = this->activeChannelIndices[itemIndex]; - snprintf(entryText, sizeof(entryText), "@%s", channels.getName(channelIndex)); + 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 && node->user.long_name) { + strncpy(entryText, node->user.long_name, sizeof(entryText) - 1); + entryText[sizeof(entryText) - 1] = '\0'; + } + int availWidth = display->getWidth() - + ((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) - + ((node && node->is_favorite) ? 10 : 0); + if (availWidth < 0) + availWidth = 0; + + size_t origLen = strlen(entryText); + while (entryText[0] && display->getStringWidth(entryText) > availWidth) { + entryText[strlen(entryText) - 1] = '\0'; + } + if (strlen(entryText) < origLen) { + strcat(entryText, "..."); + } + + // Prepend "* " if this is a favorite + if (node && node->is_favorite) { + size_t len = strlen(entryText); + if (len + 2 < sizeof(entryText)) { + memmove(entryText + 2, entryText, len + 1); + entryText[0] = '*'; + entryText[1] = ' '; + } + } if (node) { - if (node->is_favorite) { -#if defined(M5STACK_UNITC6L) - snprintf(entryText, sizeof(entryText), "* %s", node->user.short_name); - } else { + if (display->getWidth() <= 64) { snprintf(entryText, sizeof(entryText), "%s", node->user.short_name); } -#else - snprintf(entryText, sizeof(entryText), "* %s", node->user.long_name); - } else { - snprintf(entryText, sizeof(entryText), "%s", node->user.long_name); - } -#endif } } } @@ -1585,18 +1727,19 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O if (strlen(entryText) == 0 || strcmp(entryText, "Unknown") == 0) strcpy(entryText, "?"); - // === Highlight background (if selected) === + // 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 === + // Draw entry text display->drawString(xOffset + 2, yOffset, entryText); display->setColor(WHITE); - // === Draw key icon (after highlight) === + // Draw key icon (after highlight) + /* if (itemIndex >= numActiveChannels) { int nodeIndex = itemIndex - numActiveChannels; if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { @@ -1614,6 +1757,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O } } } + */ } // Scrollbar @@ -1650,6 +1794,9 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla int _visibleRows = (display->getHeight() - listTop - 2) / rowHeight; int numEmotes = graphics::numEmotes; + // keep member variable in sync + this->visibleRows = _visibleRows; + // Clamp highlight index if (emotePickerIndex < 0) emotePickerIndex = 0; @@ -1671,7 +1818,7 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla // Draw emote rows display->setTextAlignment(TEXT_ALIGN_LEFT); - for (int vis = 0; vis < visibleRows; ++vis) { + for (int vis = 0; vis < _visibleRows; ++vis) { int emoteIdx = topIndex + vis; if (emoteIdx >= numEmotes) break; @@ -1698,11 +1845,11 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla } // Draw scrollbar if needed - if (numEmotes > visibleRows) { - int scrollbarHeight = visibleRows * rowHeight; + 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 scrollBarLen = std::max(6, (scrollbarHeight * _visibleRows) / numEmotes); int scrollBarPos = listTop + (scrollbarHeight * topIndex) / numEmotes; display->fillRect(scrollTrackX, scrollBarPos, 4, scrollBarLen); } @@ -1715,104 +1862,25 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st 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); - return; + // Never draw if state is outside our UI modes + if (!(runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || + runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER)) { + return; // bail if not in a UI state that should render } - // === Emote Picker Screen === + // Emote Picker Screen if (this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { drawEmotePickerScreen(display, state, x, y); // <-- Call your emote picker drawer here return; } - // === Destination Selection === + // Destination Selection if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { drawDestinationSelectionScreen(display, state, x, y); return; } - // === ACK/NACK Screen === - if (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) { - requestFocus(); - EINK_ADD_FRAMEFLAG(display, COSMETIC); - display->setTextAlignment(TEXT_ALIGN_CENTER); - -#ifdef USE_EINK - display->setFont(FONT_SMALL); - int yOffset = y + 10; -#else - display->setFont(FONT_MEDIUM); -#if defined(M5STACK_UNITC6L) - int yOffset = y; -#else - int yOffset = y + 10; -#endif -#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); -#if defined(M5STACK_UNITC6L) - yOffset += lineCount * FONT_HEIGHT_MEDIUM - 5; // only 1 line gap, no extra padding -#else - yOffset += lineCount * FONT_HEIGHT_MEDIUM; // only 1 line gap, no extra padding -#endif -#ifndef USE_EINK - // --- SNR + RSSI Compact Line --- - if (this->ack) { - display->setFont(FONT_SMALL); -#if defined(M5STACK_UNITC6L) - snprintf(buffer, sizeof(buffer), "SNR: %.1f dB \nRSSI: %d", this->lastRxSnr, this->lastRxRssi); -#else - snprintf(buffer, sizeof(buffer), "SNR: %.1f dB RSSI: %d", this->lastRxSnr, this->lastRxRssi); -#endif - 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..."); - return; - } - - // === Disabled Screen === + // Disabled Screen if (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); @@ -1820,7 +1888,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st return; } - // === Free Text Input Screen === + // Free Text Input Screen if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { requestFocus(); #if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) @@ -1833,10 +1901,10 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - // --- Draw node/channel header at the top --- + // Draw node/channel header at the top drawHeader(display, x, y, buffer); - // --- Char count right-aligned --- + // 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); @@ -1932,51 +2000,10 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st 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; - } - } - } - 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; - } - } - } + std::vector> tokens = tokenizeMessageWithEmotes(msg); - // ===== Advanced word-wrapping (emotes + text, split by word, wrap by char if needed) ===== + // Advanced word-wrapping (emotes + text, split by word, wrap inside word if needed) std::vector>> lines; std::vector> currentLine; int lineWidth = 0; @@ -2001,7 +2028,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } else { // Text: split by words and wrap inside word if needed String text = token.second; - pos = 0; + int pos = 0; while (pos < static_cast(text.length())) { // Find next space (or end) int spacePos = text.indexOf(' ', pos); @@ -2047,18 +2074,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st int nextX = x; for (const 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; - } + // Emote rendering centralized in helper + renderEmote(display, nextX, yLine, rowHeight, token.second); } else { display->drawString(nextX, yLine, token.second); nextX += display->getStringWidth(token.second); @@ -2071,12 +2088,12 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st return; } - // === Canned Messages List === + // 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) ====== + // Precompute per-row heights based on emotes (centered if present) const int baseRowSpacing = FONT_HEIGHT_SMALL - 4; int topMsg; @@ -2096,7 +2113,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st : 0; int countRows = std::min(messagesCount, _visibleRows); - // --- Build per-row max height based on all emotes in line --- + // 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; @@ -2114,7 +2131,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st rowHeights.push_back(std::max(baseRowSpacing, maxEmoteHeight + 2)); } - // --- Draw all message rows with multi-emote support --- + // Draw all message rows with multi-emote support int yCursor = listYOffset; for (int vis = 0; vis < countRows; vis++) { int msgIdx = topMsg + vis; @@ -2123,52 +2140,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st 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 --- + // Multi-emote tokenization + std::vector> tokens = tokenizeMessageWithEmotes(msg); // Vertically center based on rowHeight int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2; @@ -2189,19 +2162,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st // Draw all tokens left to right for (const 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; - } + // Emote rendering centralized in helper + renderEmote(display, nextX, lineY, rowHeight, token.second); } else { // Text display->drawString(nextX, lineY + textYOffset, token.second); @@ -2228,43 +2190,166 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } } +// Return SNR limit based on modem preset +static float getSnrLimit(meshtastic_Config_LoRaConfig_ModemPreset preset) +{ + switch (preset) { + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: + return -6.0f; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: + return -5.5f; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: + return -4.5f; + default: + return -6.0f; + } +} + +// Return Good/Fair/Bad label and set 1–5 bars based on SNR and RSSI +static const char *getSignalGrade(float snr, int32_t rssi, float snrLimit, int &bars) +{ + // 5-bar logic: strength inside Good/Fair/Bad category + if (snr > snrLimit && rssi > -10) { + bars = 5; // very strong good + return "Good"; + } else if (snr > snrLimit && rssi > -20) { + bars = 4; // normal good + return "Good"; + } else if (snr > 0 && rssi > -50) { + bars = 3; // weaker good (on edge of fair) + return "Good"; + } else if (snr > -10 && rssi > -100) { + bars = 2; // fair + return "Fair"; + } else { + bars = 1; // bad + return "Bad"; + } +} + ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { - if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck) { + // Only process routing ACK/NACK packets that are responses to our own outbound + if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck && mp.to == nodeDB->getNodeNum() && + mp.decoded.request_id == this->lastRequestId) // only ACKs for our last sent packet + { if (mp.decoded.request_id != 0) { - // Trigger screen refresh for ACK/NACK feedback - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - requestFocus(); - this->runState = CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED; - // 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); - // Track hop metadata - this->lastAckWasRelayed = (mp.hop_limit != mp.hop_start); - this->lastAckHopStart = mp.hop_start; - this->lastAckHopLimit = mp.hop_limit; - - // Determine ACK status + // Determine ACK/NACK 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 + this->incoming = mp.from; // relayed by another node } else { - this->incoming = this->lastSentNode; // Direct reply + this->incoming = this->lastSentNode; // direct reply } - // Final ACK confirmation logic - this->ack = isAck && (wasBroadcast || isFromDest); + // Final ACK/NACK logic + if (wasBroadcast) { + // Any ACK counts for broadcast + this->ack = isAck; + waitingForAck = false; + } else if (isFromDest) { + // Only ACK from destination counts as final + this->ack = isAck; + waitingForAck = false; + } else if (isAck) { + // Relay ACK → mark as RELAYED, still no final ACK + this->ack = false; + waitingForAck = false; + } else { + // Explicit failure + this->ack = false; + waitingForAck = false; + } - waitingForAck = false; - this->notifyObservers(&e); - setIntervalFromNow(3000); // Time to show ACK/NACK screen + // Update last sent StoredMessage with ACK/NACK/RELAYED result + if (!messageStore.getMessages().empty()) { + StoredMessage &last = const_cast(messageStore.getMessages().back()); + if (last.sender == nodeDB->getNodeNum()) { // only update our own messages + if (wasBroadcast && isAck) { + last.ackStatus = AckStatus::ACKED; + } else if (isFromDest && isAck) { + last.ackStatus = AckStatus::ACKED; + } else if (!isFromDest && isAck) { + last.ackStatus = AckStatus::RELAYED; + } else { + last.ackStatus = AckStatus::NACKED; + } + } + } + + // Capture radio metrics + this->lastRxRssi = mp.rx_rssi; + this->lastRxSnr = mp.rx_snr; + + // Show overlay banner + if (screen) { + auto *display = screen->getDisplayDevice(); + graphics::BannerOverlayOptions opts; + static char buf[128]; + + const char *channelName = channels.getName(this->channel); + const char *src = getNodeName(this->incoming); + char nodeName[48]; + strncpy(nodeName, src, sizeof(nodeName) - 1); + nodeName[sizeof(nodeName) - 1] = '\0'; + + int availWidth = + display->getWidth() - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 60 : 30); + if (availWidth < 0) + availWidth = 0; + + size_t origLen = strlen(nodeName); + while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) { + nodeName[strlen(nodeName) - 1] = '\0'; + } + if (strlen(nodeName) < origLen) { + strcat(nodeName, "..."); + } + + // Calculate signal quality and bars based on preset, SNR, and RSSI + float snrLimit = getSnrLimit(config.lora.modem_preset); + int bars = 0; + const char *qualityLabel = getSignalGrade(this->lastRxSnr, this->lastRxRssi, snrLimit, bars); + + if (this->ack) { + if (this->lastSentNode == NODENUM_BROADCAST) { + snprintf(buf, sizeof(buf), "Message sent to\n#%s\n\nSignal: %s", + (channelName && channelName[0]) ? channelName : "unknown", qualityLabel); + } else { + snprintf(buf, sizeof(buf), "DM sent to\n@%s\n\nSignal: %s", + (nodeName && nodeName[0]) ? nodeName : "unknown", qualityLabel); + } + } else if (isAck && !isFromDest) { + // Relay ACK banner + snprintf(buf, sizeof(buf), "DM Relayed\n(Status Unknown)\n%s\n\nSignal: %s", + (nodeName && nodeName[0]) ? nodeName : "unknown", qualityLabel); + } else { + if (this->lastSentNode == NODENUM_BROADCAST) { + snprintf(buf, sizeof(buf), "Message failed to\n#%s", + (channelName && channelName[0]) ? channelName : "unknown"); + } else { + snprintf(buf, sizeof(buf), "DM failed to\n@%s", (nodeName && nodeName[0]) ? nodeName : "unknown"); + } + } + + opts.message = buf; + opts.durationMs = 3000; + graphics::bannerSignalBars = bars; // tell banner renderer how many bars to draw + screen->showOverlayBanner(opts); // this triggers drawNotificationBox() + } } } diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 5b0481ac7..65715dd22 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -27,10 +27,6 @@ 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 // ============================ @@ -75,7 +71,6 @@ class CannedMessageModule : public SinglePortModule, public ObservablegetMeshNode(mp.from); meshtastic_Channel ch = channels.getByIndex(mp.channel ? mp.channel : channels.getPrimaryIndex()); - if (moduleConfig.external_notification.alert_bell) { - if (containsBell) { - LOG_INFO("externalNotificationModule - Notification Bell"); + + // If we receive a broadcast message, apply channel mute setting + // If we receive a direct message and the receipent is us, apply DM mute setting + // Else we just handle it as not muted. + const bool isDmToUs = !isBroadcast(mp.to) && isToUs(&mp); + bool is_muted = isDmToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0)) + : (ch.settings.has_module_settings && ch.settings.module_settings.is_muted); + + const bool buzzerModeIsDirectOnly = + (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY); + + if (containsBell || !is_muted) { + if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message || + moduleConfig.external_notification.alert_bell_vibra || + moduleConfig.external_notification.alert_message_vibra || + ((moduleConfig.external_notification.alert_bell_buzzer || + moduleConfig.external_notification.alert_message_buzzer) && + canBuzz())) { + nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout + ? (moduleConfig.external_notification.nag_timeout * 1000) + : moduleConfig.external_notification.output_ms); + LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff); isNagging = true; + } + + if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message) { + LOG_INFO("externalNotificationModule - Notification Module or Bell"); setExternalState(0, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } } - } - if (moduleConfig.external_notification.alert_bell_vibra) { - if (containsBell) { - LOG_INFO("externalNotificationModule - Notification Bell (Vibra)"); - isNagging = true; + if (moduleConfig.external_notification.alert_bell_vibra || + moduleConfig.external_notification.alert_message_vibra) { + LOG_INFO("externalNotificationModule - Notification Module or Bell (Vibra)"); setExternalState(1, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } + + if ((moduleConfig.external_notification.alert_bell_buzzer || + moduleConfig.external_notification.alert_message_buzzer) && + canBuzz()) { + LOG_INFO("externalNotificationModule - Notification Module or Bell (Buzzer)"); + if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) { + LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - } - - if (moduleConfig.external_notification.alert_bell_buzzer && canBuzz()) { - if (containsBell) { - LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)"); - isNagging = true; - if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { - setExternalState(2, true); - } else { -#ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { - audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); - } else -#endif - if (moduleConfig.external_notification.use_pwm) { - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); - } - } - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - } - - if (moduleConfig.external_notification.alert_message && - (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { - LOG_INFO("externalNotificationModule - Notification Module"); - isNagging = true; - setExternalState(0, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - - if (moduleConfig.external_notification.alert_message_vibra && - (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { - LOG_INFO("externalNotificationModule - Notification Module (Vibra)"); - isNagging = true; - setExternalState(1, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - - if (moduleConfig.external_notification.alert_message_buzzer && - (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { - LOG_INFO("externalNotificationModule - Notification Module (Buzzer)"); - if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || - (!isBroadcast(mp.to) && isToUs(&mp))) { - // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us - isNagging = true; + // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us #ifdef T_LORA_PAGER - if (canBuzz()) { drv.setWaveform(0, 16); // Long buzzer 100% drv.setWaveform(1, 0); // Pause drv.setWaveform(2, 16); @@ -552,11 +517,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP drv.setWaveform(6, 16); drv.setWaveform(7, 0); drv.go(); - } #endif - if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { - setExternalState(2, true); - } else { #ifdef HAS_I2S if (moduleConfig.external_notification.use_i2s_as_buzzer) { audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); @@ -564,18 +525,13 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP #endif if (moduleConfig.external_notification.use_pwm) { rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + } else { + setExternalState(2, true); } } - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } else { - // Don't beep if buzzer mode is "direct messages only" and it is no direct message - LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); } } + setIntervalFromNow(0); // run once so we know if we should do something } } else { diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index f918d630f..e8fa4775a 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -1,24 +1,8 @@ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_INPUTBROKER #include "buzz/BuzzerFeedbackThread.h" -#include "input/ExpressLRSFiveWay.h" -#include "input/InputBroker.h" -#include "input/RotaryEncoderImpl.h" -#include "input/RotaryEncoderInterruptImpl1.h" -#include "input/SerialKeyboardImpl.h" -#include "input/UpDownInterruptImpl1.h" -#include "input/i2cButton.h" -#include "modules/SystemCommandsModule.h" -#if HAS_TRACKBALL -#include "input/TrackballInterruptImpl1.h" -#endif - #include "modules/StatusLEDModule.h" - -#if !MESHTASTIC_EXCLUDE_I2C -#include "input/cardKbI2cImpl.h" -#endif -#include "input/kbMatrixImpl.h" +#include "modules/SystemCommandsModule.h" #endif #if !MESHTASTIC_EXCLUDE_PKI #include "KeyVerificationModule.h" @@ -59,8 +43,6 @@ #include "modules/WaypointModule.h" #endif #if ARCH_PORTDUINO -#include "input/LinuxInputImpl.h" -#include "input/SeesawRotary.h" #include "modules/Telemetry/HostMetrics.h" #if !MESHTASTIC_EXCLUDE_STOREFORWARD #include "modules/StoreForwardModule.h" @@ -108,7 +90,13 @@ #if !MESHTASTIC_EXCLUDE_DROPZONE #include "modules/DropzoneModule.h" #endif +#if !MESHTASTIC_EXCLUDE_STATUS +#include "modules/StatusMessageModule.h" +#endif +#if defined(HAS_HARDWARE_WATCHDOG) +#include "watchdog/watchdogThread.h" +#endif /** * Create module instances here. If you are adding a new module, you must 'new' it here (or somewhere else) */ @@ -165,6 +153,9 @@ void setupModules() #if !MESHTASTIC_EXCLUDE_DROPZONE dropzoneModule = new DropzoneModule(); #endif +#if !MESHTASTIC_EXCLUDE_STATUS + statusMessageModule = new StatusMessageModule(); +#endif #if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE new GenericThreadModule(); #endif @@ -179,63 +170,6 @@ void setupModules() #endif // Example: Put your module here // new ReplyModule(); -#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { -#if defined(T_LORA_PAGER) - // use a special FSM based rotary encoder version for T-LoRa Pager - rotaryEncoderImpl = new RotaryEncoderImpl(); - if (!rotaryEncoderImpl->init()) { - delete rotaryEncoderImpl; - rotaryEncoderImpl = nullptr; - } -#elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2) - upDownInterruptImpl1 = new UpDownInterruptImpl1(); - if (!upDownInterruptImpl1->init()) { - delete upDownInterruptImpl1; - upDownInterruptImpl1 = nullptr; - } -#else - rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); - if (!rotaryEncoderInterruptImpl1->init()) { - delete rotaryEncoderInterruptImpl1; - rotaryEncoderInterruptImpl1 = nullptr; - } -#endif - cardKbI2cImpl = new CardKbI2cImpl(); - cardKbI2cImpl->init(); -#if defined(M5STACK_UNITC6L) - i2cButton = new i2cButtonThread("i2cButtonThread"); -#endif -#ifdef INPUTBROKER_MATRIX_TYPE - kbMatrixImpl = new KbMatrixImpl(); - kbMatrixImpl->init(); -#endif // INPUTBROKER_MATRIX_TYPE -#ifdef INPUTBROKER_SERIAL_TYPE - aSerialKeyboardImpl = new SerialKeyboardImpl(); - aSerialKeyboardImpl->init(); -#endif // INPUTBROKER_MATRIX_TYPE - } -#endif // HAS_BUTTON -#if ARCH_PORTDUINO - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - seesawRotary = new SeesawRotary("SeesawRotary"); - if (!seesawRotary->init()) { - delete seesawRotary; - seesawRotary = nullptr; - } - aLinuxInputImpl = new LinuxInputImpl(); - aLinuxInputImpl->init(); - } -#endif -#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL - 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 if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { cannedMessageModule = new CannedMessageModule(); @@ -252,9 +186,9 @@ void setupModules() (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { new EnvironmentTelemetryModule(); } -#if __has_include("Adafruit_PM25AQI.h") - if (moduleConfig.has_telemetry && moduleConfig.telemetry.air_quality_enabled && - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) { +#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR + if (moduleConfig.has_telemetry && + (moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled)) { new AirQualityTelemetryModule(); } #endif @@ -304,6 +238,9 @@ void setupModules() #if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS if (moduleConfig.has_range_test && moduleConfig.range_test.enabled) new RangeTestModule(); +#endif +#if defined(HAS_HARDWARE_WATCHDOG) + watchdogThread = new WatchdogThread(); #endif // NOTE! This module must be added LAST because it likes to check for replies from other modules and avoid sending extra // acks diff --git a/src/modules/NeighborInfoModule.cpp b/src/modules/NeighborInfoModule.cpp index 936a7b44a..2cd8ec5ed 100644 --- a/src/modules/NeighborInfoModule.cpp +++ b/src/modules/NeighborInfoModule.cpp @@ -170,7 +170,7 @@ bool NeighborInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, } else { LOG_DEBUG(" Ignoring dummy neighbor info packet (single neighbor with nodeId 0, snr 0)"); } - } else if (mp.hop_start != 0 && mp.hop_start == mp.hop_limit) { + } else if (getHopsAway(mp) == 0) { LOG_DEBUG("Get or create neighbor: %u with snr %f", mp.from, mp.rx_snr); // If the hopLimit is the same as hopStart, then it is a neighbor getOrCreateNeighbor(mp.from, mp.from, 0, diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index aaab019d6..a568505ae 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -2,22 +2,47 @@ #include "Default.h" #include "MeshService.h" #include "NodeDB.h" +#include "NodeStatus.h" #include "RTC.h" #include "Router.h" #include "configuration.h" #include "main.h" #include +#include + +#ifndef USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS +#define USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS (12 * 60 * 60) +#endif NodeInfoModule *nodeInfoModule; +static constexpr uint32_t NodeInfoReplySuppressSeconds = USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS; + bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr) { + suppressReplyForCurrentRequest = false; + if (mp.from == nodeDB->getNodeNum()) { LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from); return false; } auto p = *pptr; + + if (mp.decoded.want_response) { + const NodeNum sender = getFrom(&mp); + const uint32_t now = mp.rx_time ? mp.rx_time : getTime(); + auto it = lastNodeInfoSeen.find(sender); + if (it != lastNodeInfoSeen.end()) { + uint32_t sinceLast = now >= it->second ? now - it->second : 0; + if (sinceLast < NodeInfoReplySuppressSeconds) { + suppressReplyForCurrentRequest = true; + } + } + lastNodeInfoSeen[sender] = now; + pruneLastNodeInfoCache(); + } + if (p.is_licensed != owner.is_licensed) { LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!"); return true; @@ -42,6 +67,8 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes service->sendToPhone(packetCopy); } + pruneLastNodeInfoCache(); + // LOG_DEBUG("did handleReceived"); return false; // Let others look at this message also if they want } @@ -68,9 +95,11 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha if (p) { // Check whether we didn't ignore it p->to = dest; - p->decoded.want_response = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && + bool requestWantResponse = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && wantReplies; + + p->decoded.want_response = requestWantResponse; if (_shorterTimeout) p->priority = meshtastic_MeshPacket_Priority_DEFAULT; else @@ -89,19 +118,29 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha meshtastic_MeshPacket *NodeInfoModule::allocReply() { + if (suppressReplyForCurrentRequest) { + LOG_DEBUG("Skip send NodeInfo since we heard the requester <12h ago"); + ignoreRequest = true; + suppressReplyForCurrentRequest = false; + return NULL; + } + if (!airTime->isTxAllowedChannelUtil(false)) { ignoreRequest = true; // Mark it as ignored for MeshModule LOG_DEBUG("Skip send NodeInfo > 40%% ch. util"); return NULL; } - // If we sent our NodeInfo less than 5 min. ago, don't send it again as it may be still underway. - if (!shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 5 * 60 * 1000)) { - LOG_DEBUG("Skip send NodeInfo since we sent it <5min ago"); + + // Use graduated scaling based on active mesh size (10 minute base, scales with congestion coefficient) + uint32_t timeoutMs = Default::getConfiguredOrDefaultMsScaled(0, 10 * 60, nodeStatus->getNumOnline()); + if (!shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, timeoutMs)) { + LOG_DEBUG("Skip send NodeInfo since we sent it <%us ago", timeoutMs / 1000); ignoreRequest = true; // Mark it as ignored for MeshModule return NULL; } else if (shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 60 * 1000)) { + // For interactive/urgent requests (e.g., user-triggered or implicit requests), use a shorter 60s timeout LOG_DEBUG("Skip send NodeInfo since we sent it <60s ago"); - ignoreRequest = true; // Mark it as ignored for MeshModule + ignoreRequest = true; return NULL; } else { ignoreRequest = false; // Don't ignore requests anymore @@ -125,6 +164,29 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply() } } +void NodeInfoModule::pruneLastNodeInfoCache() +{ + if (!nodeDB || !nodeDB->meshNodes) + return; + + const size_t maxEntries = nodeDB->meshNodes->size(); + + for (auto it = lastNodeInfoSeen.begin(); it != lastNodeInfoSeen.end();) { + if (!nodeDB->getMeshNode(it->first)) { + it = lastNodeInfoSeen.erase(it); + } else { + ++it; + } + } + + while (!lastNodeInfoSeen.empty() && lastNodeInfoSeen.size() > maxEntries) { + auto oldestIt = std::min_element(lastNodeInfoSeen.begin(), lastNodeInfoSeen.end(), + [](const std::pair &lhs, + const std::pair &rhs) { return lhs.second < rhs.second; }); + lastNodeInfoSeen.erase(oldestIt); + } +} + NodeInfoModule::NodeInfoModule() : ProtobufModule("nodeinfo", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg), concurrency::OSThread("NodeInfo") { diff --git a/src/modules/NodeInfoModule.h b/src/modules/NodeInfoModule.h index 572b81700..d16fbeac2 100644 --- a/src/modules/NodeInfoModule.h +++ b/src/modules/NodeInfoModule.h @@ -1,5 +1,6 @@ #pragma once #include "ProtobufModule.h" +#include /** * NodeInfo module for sending/receiving NodeInfos into the mesh @@ -43,6 +44,10 @@ class NodeInfoModule : public ProtobufModule, private concurren private: uint32_t lastSentToMesh = 0; // Last time we sent our NodeInfo to the mesh bool shorterTimeout = false; + bool suppressReplyForCurrentRequest = false; + std::map lastNodeInfoSeen; + + void pruneLastNodeInfoCache(); }; extern NodeInfoModule *nodeInfoModule; diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index 8b6a9f19c..f7116e701 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -45,8 +45,12 @@ bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes { auto p = *pptr; - // If inbound message is a replay (or spoof!) of our own messages, we shouldn't process - // (why use second-hand sources for our own data?) + const auto transport = mp.transport_mechanism; + if (isFromUs(&mp) && !IS_ONE_OF(transport, meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL, + meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API)) { + LOG_WARN("Ignoring packet supposedly from us over external transport"); + return true; + } // FIXME this can in fact happen with packets sent from EUD (src=RX_SRC_USER) // to set fixed location, EUD-GPS location or just the time (see also issue #900) @@ -345,6 +349,11 @@ void PositionModule::sendOurPosition() void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t channel) { + if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) { + LOG_DEBUG("Skip position send; no fresh position since boot"); + return; + } + // cancel any not yet sent (now stale) position packets if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal) service->cancelSending(prevPacketId); @@ -416,8 +425,14 @@ int32_t PositionModule::runOnce() return RUNONCE_INTERVAL; } + bool waitingForFreshPosition = (lastGpsSend == 0) && !config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot(); + if (lastGpsSend == 0 || msSinceLastSend >= intervalMs) { - if (nodeDB->hasValidPosition(node)) { + if (waitingForFreshPosition) { +#ifdef GPS_DEBUG + LOG_DEBUG("Skip initial position send; no fresh position since boot"); +#endif + } else if (nodeDB->hasValidPosition(node)) { lastGpsSend = now; lastGpsLatitude = node->position.latitude_i; @@ -472,19 +487,53 @@ void PositionModule::sendLostAndFoundText() delete[] message; } +// Helper: return imprecise (truncated + centered) lat/lon as int32 using current precision +static inline void computeImpreciseLatLon(int32_t inLat, int32_t inLon, uint8_t precisionBits, int32_t &outLat, int32_t &outLon) +{ + if (precisionBits > 0 && precisionBits < 32) { + // Build mask for top 'precisionBits' bits of a 32-bit unsigned field + const uint32_t mask = (precisionBits == 32) ? UINT32_MAX : (UINT32_MAX << (32 - precisionBits)); + // Note: latitude_i/longitude_i are stored as signed 32-bit in meshtastic code but + // the bitmask logic used previously operated as unsigned—preserve that behavior by + // casting to uint32_t for masking, then back to int32_t. + uint32_t lat_u = static_cast(inLat) & mask; + uint32_t lon_u = static_cast(inLon) & mask; + + // Add the "center of cell" offset used elsewhere: + // The code previously added (1 << (31 - precision)) to produce the middle of the possible location. + uint32_t center_offset = (1u << (31 - precisionBits)); + lat_u += center_offset; + lon_u += center_offset; + + outLat = static_cast(lat_u); + outLon = static_cast(lon_u); + } else { + // full precision: return input unchanged + outLat = inLat; + outLon = inLon; + } +} + struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition) { - // The minimum distance to travel before we are able to send a new position packet. const uint32_t distanceTravelThreshold = Default::getConfiguredOrDefault(config.position.broadcast_smart_minimum_distance, 100); - // Determine the distance in meters between two points on the globe - float distanceTraveledSinceLastSend = GeoCoord::latLongToMeter( - lastGpsLatitude * 1e-7, lastGpsLongitude * 1e-7, currentPosition.latitude_i * 1e-7, currentPosition.longitude_i * 1e-7); + int32_t lastLatImprecise, lastLonImprecise; + int32_t currentLatImprecise, currentLonImprecise; - return SmartPosition{.distanceTraveled = abs(distanceTraveledSinceLastSend), + computeImpreciseLatLon(lastGpsLatitude, lastGpsLongitude, precision, lastLatImprecise, lastLonImprecise); + computeImpreciseLatLon(currentPosition.latitude_i, currentPosition.longitude_i, precision, currentLatImprecise, + currentLonImprecise); + + float distMeters = GeoCoord::latLongToMeter(lastLatImprecise * 1e-7, lastLonImprecise * 1e-7, currentLatImprecise * 1e-7, + currentLonImprecise * 1e-7); + + float distanceTraveled = fabsf(distMeters); + + return SmartPosition{.distanceTraveled = distanceTraveled, .distanceThreshold = distanceTravelThreshold, - .hasTraveledOverThreshold = abs(distanceTraveledSinceLastSend) >= distanceTravelThreshold}; + .hasTraveledOverThreshold = distanceTraveled >= distanceTravelThreshold}; } void PositionModule::handleNewPosition() diff --git a/src/modules/PositionModule.h b/src/modules/PositionModule.h index 4a2415058..d0a4d4603 100644 --- a/src/modules/PositionModule.h +++ b/src/modules/PositionModule.h @@ -69,10 +69,11 @@ class PositionModule : public ProtobufModule, private concu // In event mode we want to prevent excessive position broadcasts // we set the minimum interval to 5m const uint32_t minimumTimeThreshold = - max(300000, Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30)); + max(uint32_t(300000), Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, + default_broadcast_smart_minimum_interval_secs)); #else - const uint32_t minimumTimeThreshold = - Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30); + const uint32_t minimumTimeThreshold = Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, + default_broadcast_smart_minimum_interval_secs); #endif }; diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp index 05173983c..d87cf3a44 100644 --- a/src/modules/RoutingModule.cpp +++ b/src/modules/RoutingModule.cpp @@ -58,16 +58,17 @@ void RoutingModule::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketI router->sendLocal(p); // we sometimes send directly to the local node } -uint8_t RoutingModule::getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit) +uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp) { - if (hopStart != 0) { - // Hops used by the request. If somebody in between running modified firmware modified it, ignore it - uint8_t hopsUsed = hopStart < hopLimit ? config.lora.hop_limit : hopStart - hopLimit; - if (hopsUsed > config.lora.hop_limit) { + const int8_t hopsUsed = getHopsAway(mp); + if (hopsUsed >= 0) { + if (hopsUsed > (int32_t)(config.lora.hop_limit)) { // In event mode, we never want to send packets with more than our default 3 hops. #if !(EVENTMODE) // This falls through to the default. return hopsUsed; // If the request used more hops than the limit, use the same amount of hops #endif + } else if (mp.hop_start == 0) { + return 0; // The requesting node wanted 0 hops, so the response also uses a direct/local path. } else if ((uint8_t)(hopsUsed + 2) < config.lora.hop_limit) { return hopsUsed + 2; // Use only the amount of hops needed with some margin as the way back may be different } @@ -75,6 +76,12 @@ uint8_t RoutingModule::getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit } +meshtastic_MeshPacket *RoutingModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, + uint8_t hopLimit) +{ + return MeshModule::allocAckNak(err, to, idFrom, chIndex, hopLimit); +} + RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg) { isPromiscuous = true; diff --git a/src/modules/RoutingModule.h b/src/modules/RoutingModule.h index a4e0679d0..2ac42f447 100644 --- a/src/modules/RoutingModule.h +++ b/src/modules/RoutingModule.h @@ -16,8 +16,11 @@ class RoutingModule : public ProtobufModule virtual void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, bool ackWantsAck = false); + meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, + uint8_t hopLimit = 0); + // Given the hopStart and hopLimit upon reception of a request, return the hop limit to use for the response - uint8_t getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit); + uint8_t getHopLimitForResponse(const meshtastic_MeshPacket &mp); protected: friend class Router; diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 719e342b1..7a969343e 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -63,28 +63,26 @@ SerialModule *serialModule; SerialModuleRadio *serialModuleRadio; -#if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) || \ - defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || defined(ELECROW_ThinkNode_M3) || \ - defined(MUZI_BASE) -SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") -{ - api_type = TYPE_SERIAL; -} -static Print *serialPrint = &Serial; -#elif defined(CONFIG_IDF_TARGET_ESP32C6) || defined(RAK3172) || defined(EBYTE_E77_MBL) -SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("Serial") -{ - api_type = TYPE_SERIAL; -} -static Print *serialPrint = &Serial1; -#else -SerialModule::SerialModule() : StreamAPI(&Serial2), concurrency::OSThread("Serial") -{ - api_type = TYPE_SERIAL; -} -static Print *serialPrint = &Serial2; +#ifndef SERIAL_PRINT_PORT +#define SERIAL_PRINT_PORT 2 #endif +#if SERIAL_PRINT_PORT == 0 +#define SERIAL_PRINT_OBJECT Serial +#elif SERIAL_PRINT_PORT == 1 +#define SERIAL_PRINT_OBJECT Serial1 +#elif SERIAL_PRINT_PORT == 2 +#define SERIAL_PRINT_OBJECT Serial2 +#else +#error "Unsupported SERIAL_PRINT_PORT value. Allowed values are 0, 1, or 2." +#endif + +SerialModule::SerialModule() : StreamAPI(&SERIAL_PRINT_OBJECT), concurrency::OSThread("Serial") +{ + api_type = TYPE_SERIAL; +} +static Print *serialPrint = &SERIAL_PRINT_OBJECT; + char serialBytes[512]; size_t serialPayloadSize; @@ -204,8 +202,8 @@ int32_t SerialModule::runOnce() Serial.begin(baud); Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); } -#elif !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \ - !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) +#elif SERIAL_PRINT_PORT != 0 + if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { #ifdef ARCH_RP2040 Serial2.setFIFOSize(RX_BUFFER); @@ -261,8 +259,7 @@ int32_t SerialModule::runOnce() } } -#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \ - !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) +#if SERIAL_PRINT_PORT != 0 else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { processWXSerial(); @@ -536,9 +533,8 @@ ParsedLine parseLine(const char *line) */ void SerialModule::processWXSerial() { -#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) && \ - !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && \ - !defined(ARCH_STM32WL) && !defined(MUZI_BASE) +#if SERIAL_PRINT_PORT != 0 && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6) + static unsigned int lastAveraged = 0; static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded. static double dir_sum_sin = 0; diff --git a/src/modules/StatusLEDModule.cpp b/src/modules/StatusLEDModule.cpp index fc9ed310e..e7a405bdf 100644 --- a/src/modules/StatusLEDModule.cpp +++ b/src/modules/StatusLEDModule.cpp @@ -13,6 +13,8 @@ StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule") { bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); powerStatusObserver.observe(&powerStatus->onNewStatus); + if (inputBroker) + inputObserver.observe(inputBroker); } int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) @@ -20,13 +22,17 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) switch (arg->getStatusType()) { case STATUS_TYPE_POWER: { meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)arg; - if (powerStatus->getHasUSB()) { + if (powerStatus->getHasUSB() || powerStatus->getIsCharging()) { power_state = charging; if (powerStatus->getBatteryChargePercent() >= 100) { power_state = charged; } } else { - power_state = discharging; + if (powerStatus->getBatteryChargePercent() > 5) { + power_state = discharging; + } else { + power_state = critical; + } } break; } @@ -44,9 +50,10 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) break; } case meshtastic::BluetoothStatus::ConnectionState::CONNECTED: { - ble_state = connected; - PAIRING_LED_starttime = millis(); - break; + if (ble_state != connected) { + ble_state = connected; + PAIRING_LED_starttime = millis(); + } } } @@ -56,18 +63,41 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) return 0; }; +int StatusLEDModule::handleInputEvent(const InputEvent *event) +{ + lastUserbuttonTime = millis(); + return 0; +} + int32_t StatusLEDModule::runOnce() { + my_interval = 1000; if (power_state == charging) { CHARGE_LED_state = !CHARGE_LED_state; } else if (power_state == charged) { CHARGE_LED_state = LED_STATE_ON; + } else if (power_state == critical) { + if (POWER_LED_starttime + 30000 < millis() && !doing_fast_blink) { + doing_fast_blink = true; + POWER_LED_starttime = millis(); + } + if (doing_fast_blink) { + PAIRING_LED_state = LED_STATE_OFF; + CHARGE_LED_state = !CHARGE_LED_state; + my_interval = 250; + if (POWER_LED_starttime + 2000 < millis()) { + doing_fast_blink = false; + } + } else { + CHARGE_LED_state = LED_STATE_OFF; + } + } else { CHARGE_LED_state = LED_STATE_OFF; } - if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis()) { + if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis() || doing_fast_blink) { PAIRING_LED_state = LED_STATE_OFF; } else if (ble_state == unpaired) { if (slowTrack) { @@ -82,13 +112,40 @@ int32_t StatusLEDModule::runOnce() PAIRING_LED_state = LED_STATE_ON; } + bool chargeIndicatorLED1 = LED_STATE_OFF; + bool chargeIndicatorLED2 = LED_STATE_OFF; + bool chargeIndicatorLED3 = LED_STATE_OFF; + bool chargeIndicatorLED4 = LED_STATE_OFF; + if (lastUserbuttonTime + 10 * 1000 > millis() || CHARGE_LED_state == LED_STATE_ON) { + // should this be off at very low percentages? + chargeIndicatorLED1 = LED_STATE_ON; + if (powerStatus && powerStatus->getBatteryChargePercent() >= 25) + chargeIndicatorLED2 = LED_STATE_ON; + if (powerStatus && powerStatus->getBatteryChargePercent() >= 50) + chargeIndicatorLED3 = LED_STATE_ON; + if (powerStatus && powerStatus->getBatteryChargePercent() >= 75) + chargeIndicatorLED4 = LED_STATE_ON; + } + #ifdef LED_CHARGE digitalWrite(LED_CHARGE, CHARGE_LED_state); #endif - // digitalWrite(green_LED_PIN, LED_STATE_OFF); #ifdef LED_PAIRING digitalWrite(LED_PAIRING, PAIRING_LED_state); #endif +#ifdef Battery_LED_1 + digitalWrite(Battery_LED_1, chargeIndicatorLED1); +#endif +#ifdef Battery_LED_2 + digitalWrite(Battery_LED_2, chargeIndicatorLED2); +#endif +#ifdef Battery_LED_3 + digitalWrite(Battery_LED_3, chargeIndicatorLED3); +#endif +#ifdef Battery_LED_4 + digitalWrite(Battery_LED_4, chargeIndicatorLED4); +#endif + return (my_interval); } diff --git a/src/modules/StatusLEDModule.h b/src/modules/StatusLEDModule.h index d9e3a4f33..98020cb32 100644 --- a/src/modules/StatusLEDModule.h +++ b/src/modules/StatusLEDModule.h @@ -5,6 +5,7 @@ #include "PowerStatus.h" #include "concurrency/OSThread.h" #include "configuration.h" +#include "input/InputBroker.h" #include #include @@ -17,6 +18,8 @@ class StatusLEDModule : private concurrency::OSThread int handleStatusUpdate(const meshtastic::Status *); + int handleInputEvent(const InputEvent *arg); + protected: unsigned int my_interval = 1000; // interval in millisconds virtual int32_t runOnce() override; @@ -25,14 +28,19 @@ class StatusLEDModule : private concurrency::OSThread CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); CallbackObserver powerStatusObserver = CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); + CallbackObserver inputObserver = + CallbackObserver(this, &StatusLEDModule::handleInputEvent); private: bool CHARGE_LED_state = LED_STATE_OFF; bool PAIRING_LED_state = LED_STATE_OFF; uint32_t PAIRING_LED_starttime = 0; + uint32_t lastUserbuttonTime = 0; + uint32_t POWER_LED_starttime = 0; + bool doing_fast_blink = false; - enum PowerState { discharging, charging, charged }; + enum PowerState { discharging, charging, charged, critical }; PowerState power_state = discharging; diff --git a/src/modules/StatusMessageModule.cpp b/src/modules/StatusMessageModule.cpp new file mode 100644 index 000000000..139a74d8e --- /dev/null +++ b/src/modules/StatusMessageModule.cpp @@ -0,0 +1,41 @@ +#if !MESHTASTIC_EXCLUDE_STATUS + +#include "StatusMessageModule.h" +#include "MeshService.h" +#include "ProtobufModule.h" + +StatusMessageModule *statusMessageModule; + +int32_t StatusMessageModule::runOnce() +{ + if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') { + // create and send message with the status message set + meshtastic_StatusMessage ourStatus = meshtastic_StatusMessage_init_zero; + strncpy(ourStatus.status, moduleConfig.statusmessage.node_status, sizeof(ourStatus.status)); + ourStatus.status[sizeof(ourStatus.status) - 1] = '\0'; // ensure null termination + meshtastic_MeshPacket *p = allocDataPacket(); + p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), + meshtastic_StatusMessage_fields, &ourStatus); + p->to = NODENUM_BROADCAST; + p->decoded.want_response = false; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + p->channel = 0; + service->sendToMesh(p); + } + + return 1000 * 12 * 60 * 60; +} + +ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &mp) +{ + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + meshtastic_StatusMessage incomingMessage; + if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_StatusMessage_fields, + &incomingMessage)) { + LOG_INFO("Received a NodeStatus message %s", incomingMessage.status); + } + } + return ProcessMessage::CONTINUE; +} + +#endif \ No newline at end of file diff --git a/src/modules/StatusMessageModule.h b/src/modules/StatusMessageModule.h new file mode 100644 index 000000000..c9ff54018 --- /dev/null +++ b/src/modules/StatusMessageModule.h @@ -0,0 +1,35 @@ +#pragma once +#if !MESHTASTIC_EXCLUDE_STATUS +#include "SinglePortModule.h" +#include "configuration.h" + +class StatusMessageModule : public SinglePortModule, private concurrency::OSThread +{ + + public: + /** Constructor + * name is for debugging output + */ + StatusMessageModule() + : SinglePortModule("statusMessage", meshtastic_PortNum_NODE_STATUS_APP), concurrency::OSThread("StatusMessage") + { + if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') { + this->setInterval(2 * 60 * 1000); + } else { + this->setInterval(1000 * 12 * 60 * 60); + } + // TODO: If we have a string, set the initial delay (15 minutes maybe) + } + + virtual int32_t runOnce() override; + + protected: + /** Called to handle a particular incoming message + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + + private: +}; + +extern StatusMessageModule *statusMessageModule; +#endif \ No newline at end of file diff --git a/src/modules/StoreForwardModule.cpp b/src/modules/StoreForwardModule.cpp index b8a710bf5..023a1c798 100644 --- a/src/modules/StoreForwardModule.cpp +++ b/src/modules/StoreForwardModule.cpp @@ -513,7 +513,7 @@ bool StoreForwardModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, LOG_DEBUG("StoreAndForward_RequestResponse_ROUTER_BUSY"); // retry in messages_saved * packetTimeMax ms retry_delay = millis() + getNumAvailablePackets(this->busyTo, this->last_time) * packetTimeMax * - (meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR ? 2 : 1); + (p->rr == meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR ? 2 : 1); } break; diff --git a/src/modules/SystemCommandsModule.cpp b/src/modules/SystemCommandsModule.cpp index 7fa4485c8..1da756366 100644 --- a/src/modules/SystemCommandsModule.cpp +++ b/src/modules/SystemCommandsModule.cpp @@ -1,10 +1,13 @@ #include "SystemCommandsModule.h" #include "input/InputBroker.h" #include "meshUtils.h" + #if HAS_SCREEN +#include "MessageStore.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #endif + #include "GPS.h" #include "MeshService.h" #include "Module.h" @@ -28,10 +31,7 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) 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: @@ -45,10 +45,9 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) // 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->showSimpleBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);) + externalNotificationModule->setMute(!externalNotificationModule->getMute()); + IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow(); screen->showSimpleBanner( + externalNotificationModule->getMute() ? "Notifications\nDisabled" : "Notifications\nEnabled", 3000);) } return 0; // Bluetooth @@ -78,6 +77,9 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) case INPUT_BROKER_MSG_REBOOT: IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); nodeDB->saveToDisk(); +#if HAS_SCREEN + messageStore.saveToFlash(); +#endif rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; return true; diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 21a563b9d..01f5da2c6 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "AirQualityTelemetry.h" @@ -10,27 +10,54 @@ #include "PowerFSM.h" #include "RTC.h" #include "Router.h" -#include "detect/ScanI2CTwoWire.h" +#include "Sensor/AddI2CSensorTemplate.h" +#include "UnitConversions.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" #include "main.h" +#include "sleep.h" #include -#ifndef PMSA003I_WARMUP_MS -// from the PMSA003I datasheet: -// "Stable data should be got at least 30 seconds after the sensor wakeup -// from the sleep mode because of the fan’s performance." -#define PMSA003I_WARMUP_MS 30000 -#endif +// Sensors +#include "Sensor/PMSA003ISensor.h" -int32_t AirQualityTelemetryModule::runOnce() +void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { + if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { + return; + } + LOG_INFO("Air Quality Telemetry adding I2C devices..."); + /* Uncomment the preferences below if you want to use the module without having to configure it from the PythonAPI or WebUI. + Note: this was previously on runOnce, which didnt take effect + as other modules already had already been initialized (screen) */ // moduleConfig.telemetry.air_quality_enabled = 1; + // moduleConfig.telemetry.air_quality_screen_enabled = 1; + // moduleConfig.telemetry.air_quality_interval = 15; - if (!(moduleConfig.telemetry.air_quality_enabled)) { + // order by priority of metrics/values (low top, high bottom) + addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); +} + +int32_t AirQualityTelemetryModule::runOnce() +{ + if (sleepOnNextExecution == true) { + sleepOnNextExecution = false; + uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs); + LOG_DEBUG("Sleeping for %ims, then awaking to send metrics again.", nightyNightMs); + doDeepSleep(nightyNightMs, true, false); + } + + uint32_t result = UINT32_MAX; + + if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled || + AIR_QUALITY_TELEMETRY_MODULE_ENABLE)) { // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it return disable(); } @@ -42,82 +69,152 @@ int32_t AirQualityTelemetryModule::runOnce() if (moduleConfig.telemetry.air_quality_enabled) { LOG_INFO("Air quality Telemetry: init"); -#ifdef PMSA003I_ENABLE_PIN - // put the sensor to sleep on startup - pinMode(PMSA003I_ENABLE_PIN, OUTPUT); - digitalWrite(PMSA003I_ENABLE_PIN, LOW); -#endif /* PMSA003I_ENABLE_PIN */ - - if (!aqi.begin_I2C()) { -#ifndef I2C_NO_RESCAN - LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan"); - // rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty. - uint8_t i2caddr_scan[] = {PMSA0031_ADDR}; - uint8_t i2caddr_asize = 1; - auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); -#if defined(I2C_SDA1) - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); -#endif - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); - auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031); - if (found.type != ScanI2C::DeviceType::NONE) { - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = - i2cScanner->fetchI2CBus(found.address); - return setStartDelay(); - } -#endif - return disable(); + // check if we have at least one sensor + if (!sensors.empty()) { + result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - return setStartDelay(); } - return disable(); + + // it's possible to have this module enabled, only for displaying values on the screen. + // therefore, we should only enable the sensor loop if measurement is also enabled + return result == UINT32_MAX ? disable() : setStartDelay(); } else { // if we somehow got to a second run of this module with measurement disabled, then just wait forever - if (!moduleConfig.telemetry.air_quality_enabled) - return disable(); - - switch (state) { -#ifdef PMSA003I_ENABLE_PIN - case State::IDLE: - // sensor is in standby; fire it up and sleep - LOG_DEBUG("runOnce(): state = idle"); - digitalWrite(PMSA003I_ENABLE_PIN, HIGH); - state = State::ACTIVE; - - return PMSA003I_WARMUP_MS; -#endif /* PMSA003I_ENABLE_PIN */ - case State::ACTIVE: - // sensor is already warmed up; grab telemetry and send it - LOG_DEBUG("runOnce(): state = active"); - - if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && - airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && - airTime->isTxAllowedAirUtil()) { - sendTelemetry(); - lastSentToMesh = millis(); - } else if (service->isToPhoneQueueEmpty()) { - // Just send to phone when it's not our time to send to mesh yet - // Only send while queue is empty (phone assumed connected) - sendTelemetry(NODENUM_BROADCAST, true); - } - -#ifdef PMSA003I_ENABLE_PIN - // put sensor back to sleep - digitalWrite(PMSA003I_ENABLE_PIN, LOW); - state = State::IDLE; -#endif /* PMSA003I_ENABLE_PIN */ - - return sendToPhoneIntervalMs; - default: + if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { return disable(); } + + // Wake up the sensors that need it + LOG_INFO("Waking up sensors"); + for (TelemetrySensor *sensor : sensors) { + if (!sensor->isActive()) { + return sensor->wakeUp(); + } + } + + if (((lastSentToMesh == 0) || + !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && + airTime->isTxAllowedAirUtil()) { + sendTelemetry(); + lastSentToMesh = millis(); + } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && + (service->isToPhoneQueueEmpty())) { + // Just send to phone when it's not our time to send to mesh yet + // Only send while queue is empty (phone assumed connected) + sendTelemetry(NODENUM_BROADCAST, true); + lastSentToPhone = millis(); + } + + // Send to sleep sensors that consume power + LOG_INFO("Sending sensors to sleep"); + for (TelemetrySensor *sensor : sensors) { + sensor->sleep(); + } } + return min(sendToPhoneIntervalMs, result); } +bool AirQualityTelemetryModule::wantUIFrame() +{ + return moduleConfig.telemetry.air_quality_screen_enabled; +} + +#if HAS_SCREEN +void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // === Setup display === + display->clear(); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + int line = 1; + + // === Set Title + const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Air Quality" : "AQ."; + + // === 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 telemetry message from the latest received packet + const meshtastic_Data &p = lastMeasurementPacket->decoded; + 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; + } + + const auto &m = telemetry.variant.air_quality_metrics; + + // Check if any telemetry field has valid data + bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental || + m.has_pm25_environmental || m.has_pm100_environmental; + + 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_pm10_standard) + entries.push_back("PM1: " + String(m.pm10_standard) + "ug/m3"); + if (m.has_pm25_standard) + entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3"); + if (m.has_pm100_standard) + entries.push_back("PM10: " + String(m.pm100_standard) + "ug/m3"); + + // === 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; + } + graphics::drawCommonFooter(display, x, y); +} +#endif + bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { if (t->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { @@ -144,35 +241,21 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) { - if (!aqi.read(&data)) { - LOG_WARN("Skip send measurements. Could not read AQIn"); - return false; - } - + bool valid = true; + bool hasSensor = false; m->time = getTime(); m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; - m->variant.air_quality_metrics.has_pm10_standard = true; - m->variant.air_quality_metrics.pm10_standard = data.pm10_standard; - m->variant.air_quality_metrics.has_pm25_standard = true; - m->variant.air_quality_metrics.pm25_standard = data.pm25_standard; - m->variant.air_quality_metrics.has_pm100_standard = true; - m->variant.air_quality_metrics.pm100_standard = data.pm100_standard; + m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero; - m->variant.air_quality_metrics.has_pm10_environmental = true; - m->variant.air_quality_metrics.pm10_environmental = data.pm10_env; - m->variant.air_quality_metrics.has_pm25_environmental = true; - m->variant.air_quality_metrics.pm25_environmental = data.pm25_env; - m->variant.air_quality_metrics.has_pm100_environmental = true; - m->variant.air_quality_metrics.pm100_environmental = data.pm100_env; + // TODO - Should we check for sensor state here? + // If a sensor is sleeping, we should know and check to wake it up + for (TelemetrySensor *sensor : sensors) { + LOG_INFO("Reading AQ sensors"); + valid = valid && sensor->getMetrics(m); + hasSensor = true; + } - LOG_INFO("Send: PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i", m->variant.air_quality_metrics.pm10_standard, - m->variant.air_quality_metrics.pm25_standard, m->variant.air_quality_metrics.pm100_standard); - - LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", - m->variant.air_quality_metrics.pm10_environmental, m->variant.air_quality_metrics.pm25_environmental, - m->variant.air_quality_metrics.pm100_environmental); - - return true; + return valid && hasSensor; } meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() @@ -206,7 +289,15 @@ meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; + m.time = getTime(); if (getAirQualityTelemetry(&m)) { + LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \ + pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", + m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, + m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, + m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental); + meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; p->decoded.want_response = false; @@ -221,16 +312,44 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) lastMeasurementPacket = packetPool.allocCopy(*p); if (phoneOnly) { - LOG_INFO("Send packet to phone"); + LOG_INFO("Sending packet to phone"); service->sendToPhone(p); } else { - LOG_INFO("Send packet to mesh"); + LOG_INFO("Sending packet to mesh"); service->sendToMesh(p, RX_SRC_LOCAL, true); + + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { + meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); + notification->level = meshtastic_LogRecord_Level_INFO; + notification->time = getValidTime(RTCQualityFromNet); + sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", + Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs) / + 1000U); + service->sendClientNotification(notification); + sleepOnNextExecution = true; + LOG_DEBUG("Start next execution in 5s, then sleep"); + setIntervalFromNow(FIVE_SECONDS_MS); + } } return true; } - return false; } +AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; + + for (TelemetrySensor *sensor : sensors) { + result = sensor->handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + + return result; +} + #endif \ No newline at end of file diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index 0142ee686..2b88b74ba 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -1,14 +1,23 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #pragma once + +#ifndef AIR_QUALITY_TELEMETRY_MODULE_ENABLE +#define AIR_QUALITY_TELEMETRY_MODULE_ENABLE 0 +#endif + #include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "Adafruit_PM25AQI.h" #include "NodeDB.h" #include "ProtobufModule.h" +#include "detect/ScanI2CConsumer.h" +#include +#include -class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule +class AirQualityTelemetryModule : private concurrency::OSThread, + public ScanI2CConsumer, + public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, @@ -16,22 +25,19 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf public: AirQualityTelemetryModule() - : concurrency::OSThread("AirQualityTelemetry"), + : concurrency::OSThread("AirQualityTelemetry"), ScanI2CConsumer(), ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { lastMeasurementPacket = nullptr; - setIntervalFromNow(10 * 1000); - aqi = Adafruit_PM25AQI(); nodeStatusObserver.observe(&nodeStatus->onNewStatus); - -#ifdef PMSA003I_ENABLE_PIN - // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking - // a reading - state = State::IDLE; -#else - state = State::ACTIVE; -#endif + setIntervalFromNow(10 * 1000); } + virtual bool wantUIFrame() override; +#if !HAS_SCREEN + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +#else + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; +#endif protected: /** Called to handle a particular incoming message @@ -49,19 +55,17 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf */ bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); - private: - enum State { - IDLE = 0, - ACTIVE = 1, - }; + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; + void i2cScanFinished(ScanI2C *i2cScanner); - State state; - Adafruit_PM25AQI aqi; - PM25_AQI_Data data = {0}; + private: bool firstTime = true; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t lastSentToMesh = 0; + uint32_t lastSentToPhone = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 29e815092..140c2c17e 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -53,7 +53,7 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "Sensor/LTR390UVSensor.h" #endif -#if __has_include() +#if __has_include() || __has_include() #include "Sensor/BME680Sensor.h" #endif @@ -141,37 +141,10 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #define FAILED_STATE_SENSOR_READ_MULTIPLIER 10 #define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true +#include "Sensor/AddI2CSensorTemplate.h" #include "graphics/ScreenFonts.h" #include -#include - -static std::forward_list sensors; - -template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) -{ - ScanI2C::FoundDevice dev = i2cScanner->find(type); - if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { - TelemetrySensor *sensor = new T(); -#if WIRE_INTERFACES_COUNT > 1 - TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); - if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { - // This sensor only works on Wire (Wire1 is not supported) - delete sensor; - return; - } -#else - TwoWire *bus = &Wire; -#endif - if (sensor->initDevice(bus, &dev)) { - sensors.push_front(sensor); - return; - } - // destroy sensor - delete sensor; - } -} - void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { @@ -214,7 +187,7 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::LTR390UV); #endif -#if __has_include() +#if __has_include() || __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BME_680); #endif #if __has_include() @@ -378,7 +351,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt int line = 1; // === Set Title - const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env."; + const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Environment" : "Env."; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); @@ -556,37 +529,46 @@ bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPac bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m) { - bool valid = true; + bool valid = false; bool hasSensor = false; + // getMetrics() doesn't always get evaluated because of + // short-circuit evaluation rules in c++ + bool get_metrics; m->time = getTime(); m->which_variant = meshtastic_Telemetry_environment_metrics_tag; m->variant.environment_metrics = meshtastic_EnvironmentMetrics_init_zero; for (TelemetrySensor *sensor : sensors) { - valid = valid && sensor->getMetrics(m); + get_metrics = sensor->getMetrics(m); // avoid short-circuit evaluation rules + valid = valid || get_metrics; hasSensor = true; } #ifndef T1000X_SENSOR_EN if (ina219Sensor.hasSensor()) { - valid = valid && ina219Sensor.getMetrics(m); + get_metrics = ina219Sensor.getMetrics(m); + valid = valid || get_metrics; hasSensor = true; } if (ina260Sensor.hasSensor()) { - valid = valid && ina260Sensor.getMetrics(m); + get_metrics = ina260Sensor.getMetrics(m); + valid = valid || get_metrics; hasSensor = true; } if (ina3221Sensor.hasSensor()) { - valid = valid && ina3221Sensor.getMetrics(m); + get_metrics = ina3221Sensor.getMetrics(m); + valid = valid || get_metrics; hasSensor = true; } if (max17048Sensor.hasSensor()) { - valid = valid && max17048Sensor.getMetrics(m); + get_metrics = max17048Sensor.getMetrics(m); + valid = valid || get_metrics; hasSensor = true; } #endif #ifdef HAS_RAKPROT - valid = valid && rak9154Sensor.getMetrics(m); + get_metrics = rak9154Sensor.getMetrics(m); + valid = valid || get_metrics; hasSensor = true; #endif return valid && hasSensor; @@ -642,8 +624,6 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) 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); p->to = dest; p->decoded.want_response = false; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.h b/src/modules/Telemetry/EnvironmentTelemetry.h index 6e4ce82e7..049ed6b77 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.h +++ b/src/modules/Telemetry/EnvironmentTelemetry.h @@ -67,7 +67,6 @@ class EnvironmentTelemetryModule : private concurrency::OSThread, uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t lastSentToMesh = 0; uint32_t lastSentToPhone = 0; - uint32_t sensor_read_error_count = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/HealthTelemetry.cpp b/src/modules/Telemetry/HealthTelemetry.cpp index 215e49c7a..bb3555062 100644 --- a/src/modules/Telemetry/HealthTelemetry.cpp +++ b/src/modules/Telemetry/HealthTelemetry.cpp @@ -136,12 +136,12 @@ void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState * display->drawString(x, y += _fontHeight(FONT_SMALL), tempStr); if (lastMeasurement.variant.health_metrics.has_heart_bpm) { char heartStr[32]; - snprintf(heartStr, sizeof(heartStr), "Heart Rate: %.0f bpm", lastMeasurement.variant.health_metrics.heart_bpm); + snprintf(heartStr, sizeof(heartStr), "Heart Rate: %u bpm", lastMeasurement.variant.health_metrics.heart_bpm); display->drawString(x, y += _fontHeight(FONT_SMALL), heartStr); } if (lastMeasurement.variant.health_metrics.has_spO2) { char spo2Str[32]; - snprintf(spo2Str, sizeof(spo2Str), "spO2: %.0f %%", lastMeasurement.variant.health_metrics.spO2); + snprintf(spo2Str, sizeof(spo2Str), "spO2: %u %%", lastMeasurement.variant.health_metrics.spO2); display->drawString(x, y += _fontHeight(FONT_SMALL), spo2Str); } } @@ -168,18 +168,21 @@ bool HealthTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket & bool HealthTelemetryModule::getHealthTelemetry(meshtastic_Telemetry *m) { - bool valid = true; + bool valid = false; bool hasSensor = false; + bool get_metrics; m->time = getTime(); m->which_variant = meshtastic_Telemetry_health_metrics_tag; m->variant.health_metrics = meshtastic_HealthMetrics_init_zero; if (max30102Sensor.hasSensor()) { - valid = valid && max30102Sensor.getMetrics(m); + get_metrics = max30102Sensor.getMetrics(m); + valid = valid || get_metrics; // avoid short-circuit evaluation rules hasSensor = true; } if (mlx90614Sensor.hasSensor()) { - valid = valid && mlx90614Sensor.getMetrics(m); + get_metrics = mlx90614Sensor.getMetrics(m); + valid = valid || get_metrics; hasSensor = true; } diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 29dd1def8..9047c7cd4 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -117,7 +117,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s int line = 1; // === Set Title - const char *titleStr = (graphics::isHighResolution) ? "Power Telem." : "Power"; + const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Power Telem." : "Power"; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); diff --git a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h new file mode 100644 index 000000000..37d909d71 --- /dev/null +++ b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h @@ -0,0 +1,34 @@ +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR + +#include "TelemetrySensor.h" +#include "detect/ScanI2C.h" +#include "detect/ScanI2CTwoWire.h" +#include +#include + +static std::forward_list sensors; + +template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) +{ + ScanI2C::FoundDevice dev = i2cScanner->find(type); + if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { + TelemetrySensor *sensor = new T(); +#if WIRE_INTERFACES_COUNT > 1 + TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); + if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { + // This sensor only works on Wire (Wire1 is not supported) + delete sensor; + return; + } +#else + TwoWire *bus = &Wire; +#endif + if (sensor->initDevice(bus, &dev)) { + sensors.push_front(sensor); + return; + } + // destroy sensor + delete sensor; + } +} +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.cpp b/src/modules/Telemetry/Sensor/BME680Sensor.cpp index 95f3dc5f0..3a1eb9532 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BME680Sensor.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && (__has_include() || __has_include()) #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "BME680Sensor.h" @@ -10,6 +10,7 @@ BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {} +#if __has_include() int32_t BME680Sensor::runOnce() { if (!bme680.run()) { @@ -17,10 +18,13 @@ int32_t BME680Sensor::runOnce() } return 35; } +#endif bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { status = 0; + +#if __has_include() if (!bme680.begin(dev->address.address, *bus)) checkStatus("begin"); @@ -42,12 +46,25 @@ bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) if (status == 0) LOG_DEBUG("BME680Sensor::runOnce: bme680.status %d", bme680.status); +#else + bme680 = makeBME680(bus); + + if (!bme680->begin(dev->address.address)) { + LOG_ERROR("Init sensor: %s failed at begin()", sensorName); + return status; + } + + status = 1; + +#endif + initI2CSensor(); return status; } bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement) { +#if __has_include() if (bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal == 0) return false; @@ -65,9 +82,27 @@ bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement) // Check if we need to save state to filesystem (every STATE_SAVE_PERIOD ms) measurement->variant.environment_metrics.iaq = bme680.getData(BSEC_OUTPUT_IAQ).signal; updateState(); +#else + if (!bme680->performReading()) { + LOG_ERROR("BME680Sensor::getMetrics: performReading failed"); + return false; + } + + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; + measurement->variant.environment_metrics.has_gas_resistance = true; + + measurement->variant.environment_metrics.temperature = bme680->readTemperature(); + measurement->variant.environment_metrics.relative_humidity = bme680->readHumidity(); + measurement->variant.environment_metrics.barometric_pressure = bme680->readPressure() / 100.0F; + measurement->variant.environment_metrics.gas_resistance = bme680->readGas() / 1000.0; + +#endif return true; } +#if __has_include() void BME680Sensor::loadState() { #ifdef FSCom @@ -144,5 +179,6 @@ void BME680Sensor::checkStatus(const char *functionName) else if (bme680.sensor.status > BME68X_OK) LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status); } +#endif #endif diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.h b/src/modules/Telemetry/Sensor/BME680Sensor.h index f4ead95f7..eaeceb848 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.h +++ b/src/modules/Telemetry/Sensor/BME680Sensor.h @@ -1,23 +1,40 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && (__has_include() || __has_include()) #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" + +#if __has_include() +#include #include +#else +#include +#include +#endif #define STATE_SAVE_PERIOD UINT32_C(360 * 60 * 1000) // That's 6 hours worth of millis() +#if __has_include() const uint8_t bsec_config[] = { #include "config/bme680/bme680_iaq_33v_3s_4d/bsec_iaq.txt" }; - +#endif class BME680Sensor : public TelemetrySensor { private: +#if __has_include() Bsec2 bme680; +#else + using BME680Ptr = std::unique_ptr; + + static BME680Ptr makeBME680(TwoWire *bus) { return std::make_unique(bus); } + + BME680Ptr bme680; +#endif protected: +#if __has_include() const char *bsecConfigFileName = "/prefs/bsec.dat"; uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE] = {0}; uint8_t accuracy = 0; @@ -34,10 +51,13 @@ class BME680Sensor : public TelemetrySensor void loadState(); void updateState(); void checkStatus(const char *functionName); +#endif public: BME680Sensor(); +#if __has_include() virtual int32_t runOnce() override; +#endif virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp new file mode 100644 index 000000000..2225a4d87 --- /dev/null +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -0,0 +1,158 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR + +#include "../detect/reClockI2C.h" +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "PMSA003ISensor.h" +#include "TelemetrySensor.h" + +#include + +PMSA003ISensor::PMSA003ISensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") {} + +bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); +#ifdef PMSA003I_ENABLE_PIN + pinMode(PMSA003I_ENABLE_PIN, OUTPUT); +#endif + + _bus = bus; + _address = dev->address.address; + +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); + if (!currentClock) { + LOG_WARN("PMSA003I can't be used at this clock speed"); + return false; + } +#endif + + _bus->beginTransmission(_address); + if (_bus->endTransmission() != 0) { + LOG_WARN("PMSA003I not found on I2C at 0x12"); + return false; + } + +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus); +#endif + + status = 1; + LOG_INFO("PMSA003I Enabled"); + + initI2CSensor(); + return true; +} + +bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) +{ + if (!isActive()) { + LOG_WARN("PMSA003I is not active"); + return false; + } + +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); +#endif + + _bus->requestFrom(_address, PMSA003I_FRAME_LENGTH); + if (_bus->available() < PMSA003I_FRAME_LENGTH) { + LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", _bus->available()); + return false; + } + +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus); +#endif + + for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { + buffer[i] = _bus->read(); + } + + if (buffer[0] != 0x42 || buffer[1] != 0x4D) { + LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]); + return false; + } + + auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; }; + + computedChecksum = 0; + + for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH - 2; i++) { + computedChecksum += buffer[i]; + } + receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2); + + if (computedChecksum != receivedChecksum) { + LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum); + return false; + } + + measurement->variant.air_quality_metrics.has_pm10_standard = true; + measurement->variant.air_quality_metrics.pm10_standard = read16(buffer, 4); + + measurement->variant.air_quality_metrics.has_pm25_standard = true; + measurement->variant.air_quality_metrics.pm25_standard = read16(buffer, 6); + + measurement->variant.air_quality_metrics.has_pm100_standard = true; + measurement->variant.air_quality_metrics.pm100_standard = read16(buffer, 8); + + // TODO - Add admin command to remove environmental metrics to save protobuf space + measurement->variant.air_quality_metrics.has_pm10_environmental = true; + measurement->variant.air_quality_metrics.pm10_environmental = read16(buffer, 10); + + measurement->variant.air_quality_metrics.has_pm25_environmental = true; + measurement->variant.air_quality_metrics.pm25_environmental = read16(buffer, 12); + + measurement->variant.air_quality_metrics.has_pm100_environmental = true; + measurement->variant.air_quality_metrics.pm100_environmental = read16(buffer, 14); + + // TODO - Add admin command to remove PN to save protobuf space + measurement->variant.air_quality_metrics.has_particles_03um = true; + measurement->variant.air_quality_metrics.particles_03um = read16(buffer, 16); + + measurement->variant.air_quality_metrics.has_particles_05um = true; + measurement->variant.air_quality_metrics.particles_05um = read16(buffer, 18); + + measurement->variant.air_quality_metrics.has_particles_10um = true; + measurement->variant.air_quality_metrics.particles_10um = read16(buffer, 20); + + measurement->variant.air_quality_metrics.has_particles_25um = true; + measurement->variant.air_quality_metrics.particles_25um = read16(buffer, 22); + + measurement->variant.air_quality_metrics.has_particles_50um = true; + measurement->variant.air_quality_metrics.particles_50um = read16(buffer, 24); + + measurement->variant.air_quality_metrics.has_particles_100um = true; + measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26); + + return true; +} + +bool PMSA003ISensor::isActive() +{ + return state == State::ACTIVE; +} + +void PMSA003ISensor::sleep() +{ +#ifdef PMSA003I_ENABLE_PIN + digitalWrite(PMSA003I_ENABLE_PIN, LOW); + state = State::IDLE; +#endif +} + +uint32_t PMSA003ISensor::wakeUp() +{ +#ifdef PMSA003I_ENABLE_PIN + LOG_INFO("Waking up PMSA003I"); + digitalWrite(PMSA003I_ENABLE_PIN, HIGH); + state = State::ACTIVE; + return PMSA003I_WARMUP_MS; +#endif + // No need to wait for warmup if already active + return 0; +} +#endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h new file mode 100644 index 000000000..09b43d620 --- /dev/null +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -0,0 +1,35 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" + +#define PMSA003I_I2C_CLOCK_SPEED 100000 +#define PMSA003I_FRAME_LENGTH 32 +#define PMSA003I_WARMUP_MS 30000 + +class PMSA003ISensor : public TelemetrySensor +{ + public: + PMSA003ISensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + + virtual bool isActive() override; + virtual void sleep() override; + virtual uint32_t wakeUp() override; + + private: + enum class State { IDLE, ACTIVE }; + State state = State::ACTIVE; + + uint16_t computedChecksum = 0; + uint16_t receivedChecksum = 0; + + uint8_t buffer[PMSA003I_FRAME_LENGTH]{}; + TwoWire *_bus{}; + uint8_t _address{}; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp b/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp index ff0628cc3..4f3150b25 100644 --- a/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp +++ b/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp @@ -4,6 +4,15 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "RAK12035Sensor.h" +// The RAK12035 library's sensor_sleep() sets WB_IO2 (GPIO 34) LOW, which controls +// the 3.3V switched power rail (PIN_3V3_EN). This turns off power to ALL peripherals +// including GPS. We need to restore power after the library turns it off. +#ifdef PIN_3V3_EN +#define RESTORE_3V3_POWER() digitalWrite(PIN_3V3_EN, HIGH) +#else +#define RESTORE_3V3_POWER() +#endif + RAK12035Sensor::RAK12035Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_RAK12035, "RAK12035") {} bool RAK12035Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) @@ -13,16 +22,15 @@ bool RAK12035Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) delay(100); sensor.begin(dev->address.address); - // 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); + LOG_INFO("RAK12035Sensor Init Succeed \nSensor Firmware version: %i, Sensor Name: %s", data, sensorName); status = true; sensor.sensor_sleep(); + RESTORE_3V3_POWER(); } 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; @@ -38,39 +46,44 @@ bool RAK12035Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) 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; + const uint16_t default_zero_val = 510; + const uint16_t default_hundred_val = 390; + sensor.sensor_on(); + sensor.begin(); delay(200); sensor.get_dry_cal(&zero_val); + delay(200); 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); + + bool calibrationReset = false; + + if (zero_val == 0) { + LOG_INFO("Dry calibration not set, using default: %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); + delay(200); + zero_val = default_zero_val; + calibrationReset = true; } 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); + LOG_INFO("Wet calibration not set, using default: %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); + delay(200); + hundred_val = default_hundred_val; + calibrationReset = true; } + if (calibrationReset) { + LOG_INFO("Default calibration values applied. Consider running the calibration sketch for better accuracy: " + "https://github.com/RAKWireless/RAK12035_SoilMoisture"); + } + + LOG_INFO("Dry calibration value: %d, Wet calibration value: %d", zero_val, hundred_val); sensor.sensor_sleep(); + RESTORE_3V3_POWER(); delay(200); LOG_INFO("Dry calibration value is %d", zero_val); LOG_INFO("Wet calibration value is %d", hundred_val); @@ -79,10 +92,6 @@ void RAK12035Sensor::setup() 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; @@ -97,6 +106,7 @@ bool RAK12035Sensor::getMetrics(meshtastic_Telemetry *measurement) success &= sensor.get_sensor_temperature(&temp); delay(200); sensor.sensor_sleep(); + RESTORE_3V3_POWER(); if (success == false) { LOG_ERROR("Failed to read sensor data"); diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.cpp b/src/modules/Telemetry/Sensor/TelemetrySensor.cpp index d6e7d1fac..f854cb5fe 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.cpp +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "NodeDB.h" diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index 3c3e61808..af51ddfad 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -58,6 +58,11 @@ class TelemetrySensor // TODO: delete after migration bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } + // Functions to sleep / wakeup sensors that support it + virtual void sleep(){}; + virtual uint32_t wakeUp() { return 0; } + // Return active by default, override per sensor + virtual bool isActive() { return true; } #if WIRE_INTERFACES_COUNT > 1 // Set to true if Implementation only works first I2C port (Wire) @@ -65,6 +70,7 @@ class TelemetrySensor #endif virtual int32_t runOnce() { return INT32_MAX; } virtual bool isInitialized() { return initialized; } + // TODO: is this used? virtual bool isRunning() { return status > 0; } virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0; diff --git a/src/modules/TextMessageModule.cpp b/src/modules/TextMessageModule.cpp index aee359158..d94701c6b 100644 --- a/src/modules/TextMessageModule.cpp +++ b/src/modules/TextMessageModule.cpp @@ -1,10 +1,14 @@ #include "TextMessageModule.h" #include "MeshService.h" +#include "MessageStore.h" #include "NodeDB.h" #include "PowerFSM.h" #include "buzz.h" #include "configuration.h" #include "graphics/Screen.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/draw/MessageRenderer.h" +#include "main.h" TextMessageModule *textMessageModule; ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp) @@ -13,16 +17,30 @@ ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp auto &p = mp.decoded; LOG_INFO("Received text msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes); #endif + // add packet ID to the rolling list of packets + textPacketList[textPacketListIndex] = mp.id; + textPacketListIndex = (textPacketListIndex + 1) % TEXT_PACKET_LIST_SIZE; // We only store/display messages destined for us. - // Keep a copy of the most recent text message. devicestate.rx_text_message = mp; devicestate.has_rx_text_message = true; + IF_SCREEN( + // Guard against running in MeshtasticUI or with no screen + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + // Store in the central message history + const StoredMessage &sm = messageStore.addFromPacket(mp); + // Pass message to renderer (banner + thread switching + scroll reset) + // Use the global Screen singleton to retrieve the current OLED display + auto *display = screen ? screen->getDisplayDevice() : nullptr; + graphics::MessageRenderer::handleNewMessage(display, sm, mp); + }) // Only trigger screen wake if configuration allows it if (shouldWakeOnReceivedMessage()) { powerFSM.trigger(EVENT_RECEIVED_MSG); } + + // Notify any observers (e.g. external modules that care about packets) notifyObservers(&mp); return ProcessMessage::CONTINUE; // Let others look at this message also if they want @@ -32,3 +50,13 @@ bool TextMessageModule::wantPacket(const meshtastic_MeshPacket *p) { return MeshService::isTextPayload(p); } + +bool TextMessageModule::recentlySeen(uint32_t id) +{ + for (size_t i = 0; i < TEXT_PACKET_LIST_SIZE; i++) { + if (textPacketList[i] != 0 && textPacketList[i] == id) { + return true; + } + } + return false; +} \ No newline at end of file diff --git a/src/modules/TextMessageModule.h b/src/modules/TextMessageModule.h index cc0b0f9d5..42900a78e 100644 --- a/src/modules/TextMessageModule.h +++ b/src/modules/TextMessageModule.h @@ -1,9 +1,16 @@ #pragma once #include "Observer.h" #include "SinglePortModule.h" +#define TEXT_PACKET_LIST_SIZE 50 /** - * Text message handling for meshtastic - draws on the OLED display the most recent received message + * Text message handling for Meshtastic. + * + * This module is responsible for receiving and storing incoming text messages + * from the mesh. It updates device state and notifies observers so that other + * components (such as the MessageRenderer) can later display or process them. + * + * Rendering of messages on screen is no longer done here. */ class TextMessageModule : public SinglePortModule, public Observable { @@ -13,14 +20,20 @@ class TextMessageModule : public SinglePortModule, public Observable= 0) { int8_t diff = hopsTaken - *route_count; - for (uint8_t i = 0; i < diff; i++) { + for (int8_t i = 0; i < diff; i++) { if (*route_count < ROUTE_SIZE) { route[*route_count] = NODENUM_BROADCAST; // This will represent an unknown hop *route_count += 1; @@ -370,7 +371,7 @@ void TraceRouteModule::insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_Ro } // Add unknown SNR values if necessary diff = *route_count - *snr_count; - for (uint8_t i = 0; i < diff; i++) { + for (int8_t i = 0; i < diff; i++) { if (*snr_count < ROUTE_SIZE) { snr_list[*snr_count] = INT8_MIN; // This will represent an unknown SNR *snr_count += 1; diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 4b05d5fa1..4db80ba18 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -2,6 +2,7 @@ #include "NodeDB.h" #include "PowerFSM.h" #include "configuration.h" +#include "graphics/SharedUIDisplay.h" #include "graphics/draw/CompassRenderer.h" #if HAS_SCREEN @@ -14,6 +15,15 @@ WaypointModule *waypointModule; +static inline float degToRad(float deg) +{ + return deg * PI / 180.0f; +} +static inline float radToDeg(float rad) +{ + return rad * 180.0f / PI; +} + ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) @@ -52,31 +62,15 @@ 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) + if (!screen || !devicestate.has_rx_waypoint) return false; - // Decode the message, to find the expiration time (is waypoint still valid) - // This handles "deletion" as well as expiration - meshtastic_Waypoint wp; - memset(&wp, 0, sizeof(wp)); + meshtastic_Waypoint wp{}; // <- replaces memset if (pb_decode_from_bytes(devicestate.rx_waypoint.decoded.payload.bytes, devicestate.rx_waypoint.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) { - // Valid waypoint - if (wp.expire > getTime()) - return devicestate.has_rx_waypoint = true; - - // Expired, or deleted - else - return devicestate.has_rx_waypoint = false; + return wp.expire > getTime(); } - - // If decoding failed - LOG_ERROR("Failed to decode waypoint"); - devicestate.has_rx_waypoint = false; - return false; + return false; // no LOG_ERROR, no flag writes #else return false; #endif @@ -85,53 +79,46 @@ 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) + if (!screen) return; - // Prepare to draw - display->setFont(FONT_SMALL); + display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; - // Handle inverted display - // Unsure of expected behavior: for now, copy drawNodeInfo - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + // === Set Title + const char *titleStr = "Waypoint"; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + const int w = display->getWidth(); + const int h = display->getHeight(); // Decode the waypoint const meshtastic_MeshPacket &mp = devicestate.rx_waypoint; - meshtastic_Waypoint wp; - memset(&wp, 0, sizeof(wp)); + meshtastic_Waypoint wp{}; if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) { - // This *should* be caught by shouldDrawWaypoint, but we'll short-circuit here just in case - display->drawStringMaxWidth(0 + x, 0 + y, x + display->getWidth(), "Couldn't decode waypoint"); devicestate.has_rx_waypoint = false; return; } // Get timestamp info. Will pass as a field to drawColumns - static char lastStr[20]; + char lastStr[20]; getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); // Will contain distance information, passed as a field to drawColumns - static char distStr[20]; + char distStr[20]; // Get our node, to use our own position meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - // Text fields to draw (left of compass) - // Last element must be NULL. This signals the end of the char*[] to drawColumns - const char *fields[] = {"Waypoint", lastStr, wp.name, distStr, NULL}; - // Dimensions / co-ordinates for the compass/circle - int16_t compassX = 0, compassY = 0; - 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; - compassY = y + display->getHeight() / 2; - } else { - compassX = x + display->getWidth() - compassDiam / 2 - 5; - compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2; - } + const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h); + const int16_t compassX = x + w - (compassDiam / 2) - 5; + const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) + ? y + h / 2 + : y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2; // If our node has a position: if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { @@ -141,7 +128,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, myHeading = 0; } else { if (screen->hasHeading()) - myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians + myHeading = degToRad(screen->getHeading()); else myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); } @@ -157,46 +144,35 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; - bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI; + bearingToOtherDegrees = radToDeg(bearingToOtherDegrees); // Distance to Waypoint float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); 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); + float feet = d * METERS_TO_FEET; + snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°", + feet < (2 * MILES_TO_FEET) ? feet : 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); + snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000, + bearingToOtherDegrees); } - } - // If our node doesn't have position else { - // ? in the compass display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); // ? in the distance field - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - strncpy(distStr, "? mi ?°", sizeof(distStr)); - else - strncpy(distStr, "? km ?°", sizeof(distStr)); + snprintf(distStr, sizeof(distStr), "? %s ?°", + (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km"); } // Draw compass circle display->drawCircle(compassX, compassY, compassDiam / 2); - // 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) { - display->setColor(BLACK); - } - - // Must be after distStr is populated - graphics::NodeListRenderer::drawColumns(display, x, y, fields); + display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here! + display->drawString(0, graphics::getTextPositions(display)[line++], lastStr); + display->drawString(0, graphics::getTextPositions(display)[line++], wp.name); + display->drawString(0, graphics::getTextPositions(display)[line++], wp.description); + display->drawString(0, graphics::getTextPositions(display)[line++], distStr); } #endif diff --git a/src/modules/esp32/PaxcounterModule.h b/src/modules/esp32/PaxcounterModule.h index ebd6e7191..50656e32e 100644 --- a/src/modules/esp32/PaxcounterModule.h +++ b/src/modules/esp32/PaxcounterModule.h @@ -2,7 +2,7 @@ #include "ProtobufModule.h" #include "configuration.h" -#if defined(ARCH_ESP32) +#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_PAXCOUNTER #include "../mesh/generated/meshtastic/paxcount.pb.h" #include "NodeDB.h" #include @@ -35,4 +35,4 @@ class PaxcounterModule : private concurrency::OSThread, public ProtobufModule) -using namespace MotionSensorI2C; - BMA423Sensor::BMA423Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} bool BMA423Sensor::init() { - if (sensor.begin(deviceAddress(), &MotionSensorI2C::readRegister, &MotionSensorI2C::writeRegister)) { + if (sensor.begin(Wire, deviceAddress())) { sensor.configAccelerometer(sensor.RANGE_2G, sensor.ODR_100HZ, sensor.BW_NORMAL_AVG4, sensor.PERF_CONTINUOUS_MODE); sensor.enableAccelerometer(); - sensor.configInterrupt(BMA4_LEVEL_TRIGGER, BMA4_ACTIVE_HIGH, BMA4_PUSH_PULL, BMA4_OUTPUT_ENABLE, BMA4_INPUT_DISABLE); + sensor.configInterrupt(); #ifdef BMA423_INT pinMode(BMA4XX_INT, INPUT); @@ -26,9 +24,9 @@ bool BMA423Sensor::init() #ifdef T_WATCH_S3 // Need to raise the wrist function, need to set the correct axis - sensor.setReampAxes(sensor.REMAP_TOP_LAYER_RIGHT_CORNER); + sensor.setRemapAxes(sensor.REMAP_TOP_LAYER_RIGHT_CORNER); #else - sensor.setReampAxes(sensor.REMAP_BOTTOM_LAYER_BOTTOM_LEFT_CORNER); + sensor.setRemapAxes(sensor.REMAP_BOTTOM_LAYER_BOTTOM_LEFT_CORNER); #endif // sensor.enableFeature(sensor.FEATURE_STEP_CNTR, true); sensor.enableFeature(sensor.FEATURE_TILT, true); @@ -50,7 +48,7 @@ bool BMA423Sensor::init() int32_t BMA423Sensor::runOnce() { - if (sensor.readIrqStatus() != DEV_WIRE_NONE) { + if (sensor.readIrqStatus()) { if (sensor.isTilt() || sensor.isDoubleTap()) { wakeScreen(); return 500; diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp index 9455eafe0..ecada2085 100755 --- a/src/motion/ICM20948Sensor.cpp +++ b/src/motion/ICM20948Sensor.cpp @@ -47,7 +47,6 @@ int32_t ICM20948Sensor::runOnce() int32_t ICM20948Sensor::runOnce() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN -#if defined(MUZI_BASE) // temporarily gated to single device due to feature freeze if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) { if (!isAsleep) { LOG_DEBUG("sleeping IMU"); @@ -60,7 +59,6 @@ int32_t ICM20948Sensor::runOnce() sensor->sleep(false); isAsleep = false; } -#endif float magX = 0, magY = 0, magZ = 0; if (sensor->dataReady()) { diff --git a/src/motion/ICM20948Sensor.h b/src/motion/ICM20948Sensor.h index a9b7b69d0..091cb9a1e 100755 --- a/src/motion/ICM20948Sensor.h +++ b/src/motion/ICM20948Sensor.h @@ -82,8 +82,8 @@ class ICM20948Sensor : public MotionSensor private: ICM20948Singleton *sensor = nullptr; bool showingScreen = false; -#ifdef MUZI_BASE bool isAsleep = false; +#ifdef MUZI_BASE float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000, lowestZ = 98.000000; #else diff --git a/src/motion/MotionSensor.h b/src/motion/MotionSensor.h index 5039f2551..8eb3bf95b 100755 --- a/src/motion/MotionSensor.h +++ b/src/motion/MotionSensor.h @@ -61,32 +61,6 @@ class MotionSensor uint32_t endCalibrationAt = 0; }; -namespace MotionSensorI2C -{ - -static inline int readRegister(uint8_t address, uint8_t reg, uint8_t *data, uint8_t len) -{ - Wire.beginTransmission(address); - Wire.write(reg); - Wire.endTransmission(); - Wire.requestFrom((uint8_t)address, (uint8_t)len); - uint8_t i = 0; - while (Wire.available()) { - data[i++] = Wire.read(); - } - return 0; // Pass -} - -static inline int writeRegister(uint8_t address, uint8_t reg, uint8_t *data, uint8_t len) -{ - Wire.beginTransmission(address); - Wire.write(reg); - Wire.write(data, len); - return (0 != Wire.endTransmission()); -} - -} // namespace MotionSensorI2C - #endif #endif \ No newline at end of file diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index ad35e152a..18a4f913e 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -87,10 +87,13 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length) // Generate an implicit ACK towards ourselves (handled and processed only locally!) for this message. // We do this because packets are not rebroadcasted back into MQTT anymore and we assume that at least one node // receives it when we get our own packet back. Then we'll stop our retransmissions. - if (isFromUs(e.packet)) - routingModule->sendAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index); - else + if (isFromUs(e.packet)) { + auto pAck = routingModule->allocAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index); + pAck->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT; + router->sendLocal(pAck); + } else { LOG_INFO("Ignore downlink message we originally sent"); + } return; } if (isFromUs(e.packet)) { @@ -472,8 +475,10 @@ bool MQTT::publish(const char *topic, const char *payload, bool retained) if (moduleConfig.mqtt.proxy_to_client_enabled) { meshtastic_MqttClientProxyMessage *msg = mqttClientProxyMessagePool.allocZeroed(); msg->which_payload_variant = meshtastic_MqttClientProxyMessage_text_tag; - strcpy(msg->topic, topic); - strcpy(msg->payload_variant.text, payload); + strncpy(msg->topic, topic, sizeof(msg->topic)); + msg->topic[sizeof(msg->topic) - 1] = '\0'; + strncpy(msg->payload_variant.text, payload, sizeof(msg->payload_variant.text)); + msg->payload_variant.text[sizeof(msg->payload_variant.text) - 1] = '\0'; msg->retained = retained; service->sendMqttMessageToClientProxy(msg); return true; @@ -491,7 +496,8 @@ bool MQTT::publish(const char *topic, const uint8_t *payload, size_t length, boo if (moduleConfig.mqtt.proxy_to_client_enabled) { meshtastic_MqttClientProxyMessage *msg = mqttClientProxyMessagePool.allocZeroed(); msg->which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; - strlcpy(msg->topic, topic, sizeof(msg->topic)); + strncpy(msg->topic, topic, sizeof(msg->topic)); + msg->topic[sizeof(msg->topic) - 1] = '\0'; // Ensure null termination if (length > sizeof(msg->payload_variant.data.bytes)) length = sizeof(msg->payload_variant.data.bytes); msg->payload_variant.data.size = length; diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 022aa1c16..febd8339b 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -313,11 +313,11 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread { PhoneAPI::onNowHasData(fromRadioNum); +#ifdef DEBUG_NIMBLE_NOTIFY + int currentNotifyCount = notifyCount.fetch_add(1); uint8_t cc = bleServer->getConnectedCount(); - -#ifdef DEBUG_NIMBLE_NOTIFY // This logging slows things down when there are lots of packets going to the phone, like initial connection: LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc); #endif @@ -665,6 +665,9 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks #ifdef NIMBLE_TWO if (ble->isDeInit) return; +#else + if (nimbleBluetooth && nimbleBluetooth->isDeInit) + return; #endif meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); @@ -733,11 +736,7 @@ void NimbleBluetooth::deinit() isDeInit = true; #ifdef BLE_LED -#ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); -#else - digitalWrite(BLE_LED, LOW); -#endif + digitalWrite(BLE_LED, LED_STATE_OFF); #endif #ifndef NIMBLE_TWO NimBLEDevice::deinit(); diff --git a/src/platform/esp32/BleOta.cpp b/src/platform/esp32/BleOta.cpp deleted file mode 100644 index 698336f69..000000000 --- a/src/platform/esp32/BleOta.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include "BleOta.h" -#include "Arduino.h" -#include - -static const String MESHTASTIC_OTA_APP_PROJECT_NAME("Meshtastic-OTA"); - -const esp_partition_t *BleOta::findEspOtaAppPartition() -{ - const esp_partition_t *part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, nullptr); - - esp_app_desc_t app_desc; - esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - - if (ret != ESP_OK || MESHTASTIC_OTA_APP_PROJECT_NAME != app_desc.project_name) { - part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, nullptr); - ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - } - - if (ret == ESP_OK && MESHTASTIC_OTA_APP_PROJECT_NAME == app_desc.project_name) { - return part; - } else { - return nullptr; - } -} - -String BleOta::getOtaAppVersion() -{ - const esp_partition_t *part = findEspOtaAppPartition(); - esp_app_desc_t app_desc; - esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - String version; - if (ret == ESP_OK) { - version = app_desc.version; - } - return version; -} - -bool BleOta::switchToOtaApp() -{ - bool success = false; - const esp_partition_t *part = findEspOtaAppPartition(); - if (part) { - success = (ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_set_boot_partition(part)) == ESP_OK); - } - return success; -} \ No newline at end of file diff --git a/src/platform/esp32/BleOta.h b/src/platform/esp32/BleOta.h deleted file mode 100644 index f4c510920..000000000 --- a/src/platform/esp32/BleOta.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef BLEOTA_H -#define BLEOTA_H - -#include -#include - -class BleOta -{ - public: - explicit BleOta(){}; - - static String getOtaAppVersion(); - static bool switchToOtaApp(); - - private: - String mUserAgent; - static const esp_partition_t *findEspOtaAppPartition(); -}; - -#endif // BLEOTA_H \ No newline at end of file diff --git a/src/platform/esp32/WiFiOTA.cpp b/src/platform/esp32/MeshtasticOTA.cpp similarity index 50% rename from src/platform/esp32/WiFiOTA.cpp rename to src/platform/esp32/MeshtasticOTA.cpp index 4cf157b4c..4ca074723 100644 --- a/src/platform/esp32/WiFiOTA.cpp +++ b/src/platform/esp32/MeshtasticOTA.cpp @@ -1,13 +1,17 @@ -#include "WiFiOTA.h" +#include "MeshtasticOTA.h" #include "configuration.h" +#ifdef ESP_PLATFORM #include #include +#endif -namespace WiFiOTA +namespace MeshtasticOTA { -static const char *nvsNamespace = "ota-wifi"; -static const char *appProjectName = "OTA-WiFi"; +static const char *nvsNamespace = "MeshtasticOTA"; +static const char *combinedAppProjectName = "MeshtasticOTA"; +static const char *bleOnlyAppProjectName = "MeshtasticOTA-BLE"; +static const char *wifiOnlyAppProjectName = "MeshtasticOTA-WiFi"; static bool updated = false; @@ -43,12 +47,14 @@ void recoverConfig(meshtastic_Config_NetworkConfig *network) strncpy(network->wifi_psk, psk.c_str(), sizeof(network->wifi_psk)); } -void saveConfig(meshtastic_Config_NetworkConfig *network) +void saveConfig(meshtastic_Config_NetworkConfig *network, meshtastic_OTAMode method, uint8_t *ota_hash) { LOG_INFO("Saving WiFi settings for upcoming OTA update"); Preferences prefs; prefs.begin(nvsNamespace); + prefs.putUChar("method", method); + prefs.putBytes("ota_hash", ota_hash, 32); prefs.putString("ssid", network->wifi_ssid); prefs.putString("psk", network->wifi_psk); prefs.putBool("updated", false); @@ -62,21 +68,48 @@ const esp_partition_t *getAppPartition() bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc) { - if (esp_ota_get_partition_description(part, app_desc) != ESP_OK) - return false; - if (strcmp(app_desc->project_name, appProjectName) != 0) + if (esp_ota_get_partition_description(part, app_desc) != ESP_OK) { + LOG_INFO("esp_ota_get_partition_description failed"); return false; + } return true; } +bool checkOTACapability(esp_app_desc_t *app_desc, uint8_t method) +{ + // Combined loader supports all (both) transports, BLE and WiFi + if (strcmp(app_desc->project_name, combinedAppProjectName) == 0) { + LOG_INFO("OTA partition contains combined BLE/WiFi OTA Loader"); + return true; + } + if (method == METHOD_OTA_BLE && strcmp(app_desc->project_name, bleOnlyAppProjectName) == 0) { + LOG_INFO("OTA partition contains BLE-only OTA Loader"); + return true; + } + if (method == METHOD_OTA_WIFI && strcmp(app_desc->project_name, wifiOnlyAppProjectName) == 0) { + LOG_INFO("OTA partition contains WiFi-only OTA Loader"); + return true; + } + LOG_INFO("OTA partition does not contain a known OTA loader"); + return false; +} + bool trySwitchToOTA() { const esp_partition_t *part = getAppPartition(); - esp_app_desc_t app_desc; - if (!getAppDesc(part, &app_desc)) + + if (part == NULL) { + LOG_WARN("Unable to get app partition in preparation of OTA reboot"); return false; - if (esp_ota_set_boot_partition(part) != ESP_OK) + } + + uint8_t result = esp_ota_set_boot_partition(part); + // Partition and app checks should now be done in the AdminModule before this is called + if (result != ESP_OK) { + LOG_WARN("Unable to switch to OTA partiton. (Reason %d)", result); return false; + } + return true; } @@ -89,4 +122,4 @@ const char *getVersion() return app_desc.version; } -} // namespace WiFiOTA +} // namespace MeshtasticOTA diff --git a/src/platform/esp32/MeshtasticOTA.h b/src/platform/esp32/MeshtasticOTA.h new file mode 100644 index 000000000..7c158775f --- /dev/null +++ b/src/platform/esp32/MeshtasticOTA.h @@ -0,0 +1,26 @@ +#ifndef MESHTASTICOTA_H +#define MESHTASTICOTA_H + +#include "mesh-pb-constants.h" +#include +#ifdef ESP_PLATFORM +#include +#endif + +#define METHOD_OTA_BLE 1 +#define METHOD_OTA_WIFI 2 + +namespace MeshtasticOTA +{ +void initialize(); +bool isUpdated(); +const esp_partition_t *getAppPartition(); +bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc); +bool checkOTACapability(esp_app_desc_t *app_desc, uint8_t method); +void recoverConfig(meshtastic_Config_NetworkConfig *network); +void saveConfig(meshtastic_Config_NetworkConfig *network, meshtastic_OTAMode method, uint8_t *ota_hash); +bool trySwitchToOTA(); +const char *getVersion(); +} // namespace MeshtasticOTA + +#endif // MESHTASTICOTA_H diff --git a/src/platform/esp32/WiFiOTA.h b/src/platform/esp32/WiFiOTA.h deleted file mode 100644 index 5a7ee348a..000000000 --- a/src/platform/esp32/WiFiOTA.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef WIFIOTA_H -#define WIFIOTA_H - -#include "mesh-pb-constants.h" -#include - -namespace WiFiOTA -{ -void initialize(); -bool isUpdated(); - -void recoverConfig(meshtastic_Config_NetworkConfig *network); -void saveConfig(meshtastic_Config_NetworkConfig *network); -bool trySwitchToOTA(); -const char *getVersion(); -} // namespace WiFiOTA - -#endif // WIFIOTA_H diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 085692f96..7aee45f81 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -33,9 +33,6 @@ #ifndef HAS_RADIO #define HAS_RADIO 1 #endif -#ifndef HAS_RTC -#define HAS_RTC 1 -#endif #ifndef HAS_CPU_SHUTDOWN #define HAS_CPU_SHUTDOWN 1 #endif @@ -195,6 +192,8 @@ #define HW_VENDOR meshtastic_HardwareModel_LINK_32 #elif defined(T_DECK_PRO) #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO +#elif defined(T_BEAM_1W) +#define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT #elif defined(T_LORA_PAGER) #define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER #elif defined(HELTEC_V4) diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp index 760964119..6667acf5c 100644 --- a/src/platform/esp32/main-esp32.cpp +++ b/src/platform/esp32/main-esp32.cpp @@ -5,11 +5,10 @@ #include "main.h" #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH -#include "BleOta.h" #include "nimble/NimbleBluetooth.h" #endif -#include +#include #if HAS_WIFI #include "mesh/wifi/WiFiAPClient.h" @@ -144,22 +143,14 @@ void esp32Setup() preferences.putUInt("hwVendor", HW_VENDOR); preferences.end(); LOG_DEBUG("Number of Device Reboots: %d", rebootCounter); -#if !MESHTASTIC_EXCLUDE_BLUETOOTH - String BLEOTA = BleOta::getOtaAppVersion(); - if (BLEOTA.isEmpty()) { - LOG_INFO("No BLE OTA firmware available"); - } else { - LOG_INFO("BLE OTA firmware version %s", BLEOTA.c_str()); - } -#endif #if !MESHTASTIC_EXCLUDE_WIFI - String version = WiFiOTA::getVersion(); + String version = MeshtasticOTA::getVersion(); if (version.isEmpty()) { - LOG_INFO("No WiFi OTA firmware available"); + LOG_INFO("MeshtasticOTA firmware not available"); } else { - LOG_INFO("WiFi OTA firmware version %s", version.c_str()); + LOG_INFO("MeshtasticOTA firmware version %s", version.c_str()); } - WiFiOTA::initialize(); + MeshtasticOTA::initialize(); #endif // enableModemSleep(); diff --git a/src/platform/extra_variants/README.md b/src/platform/extra_variants/README.md index e558502f0..838014c4f 100644 --- a/src/platform/extra_variants/README.md +++ b/src/platform/extra_variants/README.md @@ -5,7 +5,7 @@ This directory tree is designed to solve two problems. - The ESP32 arduino/platformio project doesn't support the nice "if initVariant() is found, call that after init" behavior of the nrf52 builds (they use initVariant() internally). - Over the years a lot of 'board specific' init code has been added to init() in main.cpp. It would be great to have a general/clean mechanism to allow developers to specify board specific/unique code in a clean fashion without mucking in main. -So we are borrowing the initVariant() ideas here (by using weak gcc references). You can now define lateInitVariant() if your board needs it. +So we are borrowing the initVariant() ideas here (by using weak gcc references). You can now define earlyInitVariant() and lateInitVariant() if your board needs them. earlyInitVariant() runs at the beginning of setup() directly after waitUntilPowerLevelSafe(); while lateInitVariant() runs after the LoRa radio is initialized. If you'd like a board specific variant to be run, add the variant.cpp file to an appropriately named subdirectory and check for \_VARIANT_boardname in the cpp file (so that your code is only built for your board). diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 34a185b28..960cdf208 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -119,7 +119,7 @@ void startAdv(void) Bluefruit.Advertising.addService(meshBleService); /* Start Advertising * - Enable auto advertising if disconnected - * - Interval: fast mode = 20 ms, slow mode = 152.5 ms + * - Interval: fast mode = 20 ms, slow mode = 417,5 ms * - Timeout for fast mode is 30 seconds * - Start(timeout) with timeout = 0 will advertise forever (until connected) * @@ -127,7 +127,7 @@ void startAdv(void) * https://developer.apple.com/library/content/qa/qa1931/_index.html */ Bluefruit.Advertising.restartOnDisconnect(true); - Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setInterval(32, 668); // in unit of 0.625 ms Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds. FIXME, we should stop advertising after X } @@ -240,6 +240,12 @@ int NRF52Bluetooth::getRssi() { return 0; // FIXME figure out where to source this } + +// Valid BLE TX power levels as per nRF52840 Product Specification are: "-20 to +8 dBm TX power, configurable in 4 dB steps". +// See https://docs.nordicsemi.com/bundle/ps_nrf52840/page/keyfeatures_html5.html +#define VALID_BLE_TX_POWER(x) \ + ((x) == -20 || (x) == -16 || (x) == -12 || (x) == -8 || (x) == -4 || (x) == 0 || (x) == 4 || (x) == 8) + void NRF52Bluetooth::setup() { // Initialise the Bluefruit module @@ -251,6 +257,9 @@ void NRF52Bluetooth::setup() Bluefruit.Advertising.stop(); Bluefruit.Advertising.clearData(); Bluefruit.ScanResponse.clearData(); +#if defined(NRF52_BLE_TX_POWER) && VALID_BLE_TX_POWER(NRF52_BLE_TX_POWER) + Bluefruit.setTxPower(NRF52_BLE_TX_POWER); +#endif if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { configuredPasskey = config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN ? config.bluetooth.fixed_pin @@ -272,6 +281,29 @@ void NRF52Bluetooth::setup() // Set the connect/disconnect callback handlers Bluefruit.Periph.setConnectCallback(onConnect); Bluefruit.Periph.setDisconnectCallback(onDisconnect); + + // Do not change Slave Latency to value other than 0 !!! + // There is probably a bug in SoftDevice + certain Apple iOS versions being + // brain damaged causing connectivity problems. + + // On one side it seems SoftDevice is using SlaveLatency value even + // if connection parameter negotation failed and phone sees it as connectivity errors. + + // On the other hand Apple can randomly refuse any parameter negotiation and shutdown connection + // even if you meet Apple Developer Guidelines for BLE devices. Because f* you, that's why. + + // While this API call sets preferred connection parameters (PPCP) - many phones ignore it (yeah) and it seems SoftDevice + // will try to renegotiate connection parameters based on those values after phone connection. + // So those are relatively safe values so Apple braindead firmware won't get angry and at least we may try + // to negotiate some longer connection interval to save battery. + + // See https://github.com/meshtastic/firmware/pull/8858 for measurements. We are dealing with microamp savings anyway so not + // worth dying on a hill here. + + Bluefruit.Periph.setConnSlaveLatency(0); + // 1.25 ms units - so min, max is 15, 100 ms range. + Bluefruit.Periph.setConnInterval(12, 80); + #ifndef BLE_DFU_SECURE bledfu.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); bledfu.begin(); // Install the DFU helper @@ -300,7 +332,7 @@ void NRF52Bluetooth::setup() void NRF52Bluetooth::resumeAdvertising() { Bluefruit.Advertising.restartOnDisconnect(true); - Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setInterval(32, 668); // in unit of 0.625 ms Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode Bluefruit.Advertising.start(0); } diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index d4699cd8c..d1965f03e 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -5,6 +5,25 @@ // // defaults for NRF52 architecture // + +/* + * Internal Reference is +/-0.6V, with an adjustable gain of 1/6, 1/5, 1/4, + * 1/3, 1/2 or 1, meaning 3.6, 3.0, 2.4, 1.8, 1.2 or 0.6V for the ADC levels. + * + * External Reference is VDD/4, with an adjustable gain of 1, 2 or 4, meaning + * VDD/4, VDD/2 or VDD for the ADC levels. + * + * Default settings are internal reference with 1/6 gain (GND..3.6V ADC range) + * Some variants overwrite it. + */ +#ifndef AREF_VOLTAGE +#define AREF_VOLTAGE 3.6 +#endif + +#ifndef BATTERY_SENSE_RESOLUTION_BITS +#define BATTERY_SENSE_RESOLUTION_BITS 10 +#endif + #ifndef HAS_BLUETOOTH #define HAS_BLUETOOTH 1 #endif @@ -66,12 +85,16 @@ #define HW_VENDOR meshtastic_HardwareModel_T_ECHO #elif defined(T_ECHO_LITE) #define HW_VENDOR meshtastic_HardwareModel_T_ECHO_LITE +#elif defined(TTGO_T_ECHO_PLUS) +#define HW_VENDOR meshtastic_HardwareModel_T_ECHO_PLUS #elif defined(ELECROW_ThinkNode_M1) #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M1 #elif defined(ELECROW_ThinkNode_M3) #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M3 #elif defined(ELECROW_ThinkNode_M6) #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M6 +#elif defined(ELECROW_ThinkNode_M4) +#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M4 #elif defined(NANO_G2_ULTRA) #define HW_VENDOR meshtastic_HardwareModel_NANO_G2_ULTRA #elif defined(CANARYONE) diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 5d1ba20ba..0376a1dad 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -9,12 +9,12 @@ #define NRFX_WDT_ENABLED 1 #define NRFX_WDT0_ENABLED 1 #define NRFX_WDT_CONFIG_NO_IRQ 1 -#include -#include - +#include "nrfx_power.h" #include #include #include +#include +#include #include // #include #include "NodeDB.h" @@ -23,6 +23,7 @@ #include "main.h" #include "meshUtils.h" #include "power.h" +#include #include @@ -30,6 +31,21 @@ #include "BQ25713.h" #endif +// WARNING! THRESHOLD + HYSTERESIS should be less than regulated VDD voltage - which depends on board +// and is 3.0 or 3.3V. Also VDD likes to read values like 2.9999 so make sure you account for that +// otherwise board will not boot at all. Before you modify this part - please triple read NRF52840 power design +// section in datasheet and you understand how REG0 and REG1 regulators work together. +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD +#define SAFE_VDD_VOLTAGE_THRESHOLD 2.7 +#endif + +// hysteresis value +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD_HYST +#define SAFE_VDD_VOLTAGE_THRESHOLD_HYST 0.2 +#endif + +uint16_t getVDDVoltage(); + // Weak empty variant initialization function. // May be redefined by variant files. void variant_shutdown() __attribute__((weak)); @@ -38,12 +54,95 @@ void variant_shutdown() {} static nrfx_wdt_t nrfx_wdt = NRFX_WDT_INSTANCE(0); static nrfx_wdt_channel_id nrfx_wdt_channel_id_nrf52_main; +// This is a public global so that the debugger can set it to false automatically from our gdbinit +// @phaseloop comment: most part of codebase, including filesystem flash driver depend on softdevice +// methods so disabling it may actually crash thing. Proceed with caution. + +bool useSoftDevice = true; // Set to false for easier debugging + static inline void debugger_break(void) { __asm volatile("bkpt #0x01\n\t" "mov pc, lr\n\t"); } +// PowerHAL NRF52 specific function implementations +bool powerHAL_isVBUSConnected() +{ + return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk; +} + +bool powerHAL_isPowerLevelSafe() +{ + + static bool powerLevelSafe = true; + + uint16_t threshold = SAFE_VDD_VOLTAGE_THRESHOLD * 1000; // convert V to mV + uint16_t hysteresis = SAFE_VDD_VOLTAGE_THRESHOLD_HYST * 1000; + + if (powerLevelSafe) { + if (getVDDVoltage() < threshold) { + powerLevelSafe = false; + } + } else { + // power level is only safe again when it raises above threshold + hysteresis + if (getVDDVoltage() >= (threshold + hysteresis)) { + powerLevelSafe = true; + } + } + + return powerLevelSafe; +} + +void powerHAL_platformInit() +{ + + // Enable POF power failure comparator. It will prevent writing to NVMC flash when supply voltage is too low. + // Set to some low value as last resort - powerHAL_isPowerLevelSafe uses different method and should manage proper node + // behaviour on its own. + + // POFWARN is pretty useless for node power management because it triggers only once and clearing this event will not + // re-trigger it again until voltage rises to safe level and drops again. So we will use SAADC routed to VDD to read safely + // voltage. + + // @phaseloop: I disable POFCON for now because it seems to be unreliable or buggy. Even when set at 2.0V it + // triggers below 2.8V and corrupts data when pairing bluetooth - because it prevents filesystem writes and + // adafruit BLE library triggers lfs_assert which reboots node and formats filesystem. + // I did experiments with bench power supply and no matter what is set to POFCON, it always triggers right below + // 2.8V. I compared raw registry values with datasheet. + + NRF_POWER->POFCON = + ((POWER_POFCON_THRESHOLD_V22 << POWER_POFCON_THRESHOLD_Pos) | (POWER_POFCON_POF_Enabled << POWER_POFCON_POF_Pos)); + + // remember to always match VBAT_AR_INTERNAL with AREF_VALUE in variant definition file +#ifdef VBAT_AR_INTERNAL + analogReference(VBAT_AR_INTERNAL); +#else + analogReference(AR_INTERNAL); // 3.6V +#endif +} + +// get VDD voltage (in millivolts) +uint16_t getVDDVoltage() +{ + // we use the same values as regular battery read so there is no conflict on SAADC + analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); + + // VDD range on NRF52840 is 1.8-3.3V so we need to remap analog reference to 3.6V + // let's hope battery reading runs in same task and we don't have race condition + analogReference(AR_INTERNAL); + + uint16_t vddADCRead = analogReadVDD(); + float voltage = ((1000 * 3.6) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vddADCRead; + +// restore default battery reading reference +#ifdef VBAT_AR_INTERNAL + analogReference(VBAT_AR_INTERNAL); +#endif + + return voltage; +} + bool loopCanSleep() { // turn off sleep only while connected via USB @@ -72,22 +171,6 @@ void getMacAddr(uint8_t *dmac) dmac[0] = src[5] | 0xc0; // MSB high two bits get set elsewhere in the bluetooth stack } -static void initBrownout() -{ - auto vccthresh = POWER_POFCON_THRESHOLD_V24; - - auto err_code = sd_power_pof_enable(POWER_POFCON_POF_Enabled); - assert(err_code == NRF_SUCCESS); - - err_code = sd_power_pof_threshold_set(vccthresh); - assert(err_code == NRF_SUCCESS); - - // We don't bother with setting up brownout if soft device is disabled - because during production we always use softdevice -} - -// This is a public global so that the debugger can set it to false automatically from our gdbinit -bool useSoftDevice = true; // Set to false for easier debugging - #if !MESHTASTIC_EXCLUDE_BLUETOOTH void setBluetoothEnable(bool enable) { @@ -106,7 +189,6 @@ void setBluetoothEnable(bool enable) if (!initialized) { nrf52Bluetooth = new NRF52Bluetooth(); nrf52Bluetooth->startDisabled(); - initBrownout(); initialized = true; } return; @@ -120,9 +202,6 @@ void setBluetoothEnable(bool enable) LOG_DEBUG("Init NRF52 Bluetooth"); nrf52Bluetooth = new NRF52Bluetooth(); nrf52Bluetooth->setup(); - - // We delay brownout init until after BLE because BLE starts soft device - initBrownout(); } // Already setup, apparently else @@ -192,9 +271,24 @@ extern "C" void lfs_assert(const char *reason) delay(500); // Give the serial port a bit of time to output that last message. // Try setting GPREGRET with the SoftDevice first. If that fails (perhaps because the SD hasn't been initialize yet) then set // NRF_POWER->GPREGRET directly. - if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && sd_power_gpregret_set(0, NRF52_MAGIC_LFS_IS_CORRUPT) == NRF_SUCCESS)) { - NRF_POWER->GPREGRET = NRF52_MAGIC_LFS_IS_CORRUPT; + + // TODO: this will/can crash CPU if bluetooth stack is not compiled in or bluetooth is not initialized + // (regardless if enabled or disabled) - as there is no live SoftDevice stack + // implement "safe" functions detecting softdevice stack state and using proper method to set registers + + // do not set GPREGRET if POFWARN is triggered because it means lfs_assert reports flash undervoltage protection + // and not data corruption. Reboot is fine as boot procedure will wait until power level is safe again + + if (!NRF_POWER->EVENTS_POFWARN) { + if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && + sd_power_gpregret_set(0, NRF52_MAGIC_LFS_IS_CORRUPT) == NRF_SUCCESS)) { + NRF_POWER->GPREGRET = NRF52_MAGIC_LFS_IS_CORRUPT; + } } + + // TODO: this should not be done when SoftDevice is enabled as device will not boot back on soft reset + // as some data is retained in RAM which will prevent re-enabling bluetooth stack + // Google what Nordic has to say about NVIC_* + SoftDevice NVIC_SystemReset(); } @@ -336,15 +430,6 @@ void cpuDeepSleep(uint32_t msecToWake) Serial1.end(); #endif -#ifdef TTGO_T_ECHO - // To power off the T-Echo, the display must be set - // as an input pin; otherwise, there will be leakage current. - pinMode(PIN_EINK_CS, INPUT); - pinMode(PIN_EINK_DC, INPUT); - pinMode(PIN_EINK_RES, INPUT); - pinMode(PIN_EINK_BUSY, INPUT); -#endif - setBluetoothEnable(false); #ifdef RAK4630 @@ -355,57 +440,8 @@ void cpuDeepSleep(uint32_t msecToWake) // RAK-12039 set pin for Air quality sensor digitalWrite(AQ_SET_PIN, LOW); #endif -#ifdef RAK14014 - // GPIO restores input status, otherwise there will be leakage current - nrf_gpio_cfg_default(TFT_BL); - nrf_gpio_cfg_default(TFT_DC); - nrf_gpio_cfg_default(TFT_CS); - nrf_gpio_cfg_default(TFT_SCLK); - nrf_gpio_cfg_default(TFT_MOSI); - nrf_gpio_cfg_default(TFT_MISO); - 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 -#ifdef PIN_WD_EN - digitalWrite(PIN_WD_EN, LOW); -#endif -#endif - -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_MESH_SOLAR) - nrf_gpio_cfg_default(PIN_GPS_PPS); - detachInterrupt(PIN_GPS_PPS); - detachInterrupt(PIN_BUTTON1); -#endif - -#ifdef ELECROW_ThinkNode_M1 - for (int pin = 0; pin < 48; pin++) { - if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || - pin == PIN_BUTTON1 || pin == PIN_BUTTON2) { - continue; - } - pinMode(pin, OUTPUT); - } - for (int pin = 0; pin < 48; pin++) { - if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || - pin == PIN_BUTTON1 || pin == PIN_BUTTON2) { - continue; - } - digitalWrite(pin, LOW); - } - for (int pin = 0; pin < 48; pin++) { - if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || - pin == PIN_BUTTON1 || pin == PIN_BUTTON2) { - continue; - } - NRF_GPIO->DIRCLR = (1 << pin); - } #endif + // Run shutdown code if specified in variant.cpp variant_shutdown(); // Sleepy trackers or sensors can low power "sleep" @@ -428,22 +464,6 @@ void cpuDeepSleep(uint32_t msecToWake) // FIXME, use non-init RAM per // https://devzone.nordicsemi.com/f/nordic-q-a/48919/ram-retention-settings-with-softdevice-enabled -#ifdef ELECROW_ThinkNode_M1 - nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input - nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; - nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense); - - nrf_gpio_cfg_input(PIN_BUTTON2, NRF_GPIO_PIN_PULLUP); - nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; - nrf_gpio_cfg_sense_set(PIN_BUTTON2, sense1); -#endif - -#ifdef PROMICRO_DIY_TCXO - nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Enable internal pull-up on the button pin - nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; // Configure SENSE signal on low edge - nrf_gpio_cfg_sense_set(BUTTON_PIN, sense); // Apply SENSE to wake up the device from the deep sleep -#endif - #ifdef BATTERY_LPCOMP_INPUT // Wake up if power rises again nrf_lpcomp_config_t c; diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index 10b3a7fe4..9579bef45 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -6,6 +6,7 @@ #include "target_specific.h" #include "PortduinoGlue.h" +#include "SHA256.h" #include "api/ServerAPI.h" #include "linux/gpio/LinuxGPIOPin.h" #include "meshUtils.h" @@ -18,6 +19,7 @@ #include #include #include +#include #include #include @@ -28,7 +30,9 @@ #include "platform/portduino/USBHal.h" portduino_config_struct portduino_config; +portduino_status_struct portduino_status; std::ofstream traceFile; +std::ofstream JSONFile; Ch341Hal *ch341Hal = nullptr; char *configPath = nullptr; char *optionMac = nullptr; @@ -53,15 +57,18 @@ void cpuDeepSleep(uint32_t msecs) void updateBatteryLevel(uint8_t level) NOT_IMPLEMENTED("updateBatteryLevel"); int TCPPort = SERVER_API_DEFAULT_PORT; +bool checkConfigPort = true; static error_t parse_opt(int key, char *arg, struct argp_state *state) { switch (key) { case 'p': - if (sscanf(arg, "%d", &TCPPort) < 1) + if (sscanf(arg, "%d", &TCPPort) < 1) { return ARGP_ERR_UNKNOWN; - else + } else { + checkConfigPort = false; printf("Using config file %d\n", TCPPort); + } break; case 'c': configPath = arg; @@ -269,7 +276,39 @@ void portduinoSetup() } std::cout << "autoconf: Found Pi HAT+ " << hat_vendor << " " << autoconf_product << " at /proc/device-tree/hat" << std::endl; - found_hat = true; + + // potential TODO: Validate that this is a real UUID + std::ifstream hatUUID("/proc/device-tree/hat/uuid"); + char uuid[38] = {0}; + if (hatUUID.is_open()) { + hatUUID.read(uuid, 37); + hatUUID.close(); + std::cout << "autoconf: UUID " << uuid << std::endl; + SHA256 uuid_hash; + uint8_t uuid_hash_bytes[32] = {0}; + + uuid_hash.reset(); + uuid_hash.update(uuid, 37); + uuid_hash.finalize(uuid_hash_bytes, 32); + + for (int j = 0; j < 16; j++) { + portduino_config.device_id[j] = uuid_hash_bytes[j]; + } + portduino_config.has_device_id = true; + uint8_t dmac[6] = {0}; + dmac[0] = (uuid_hash_bytes[17] << 4) | 2; + dmac[1] = uuid_hash_bytes[18]; + dmac[2] = uuid_hash_bytes[19]; + dmac[3] = uuid_hash_bytes[20]; + dmac[4] = uuid_hash_bytes[21]; + dmac[5] = uuid_hash_bytes[22]; + char macBuf[13] = {0}; + snprintf(macBuf, sizeof(macBuf), "%02X%02X%02X%02X%02X%02X", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], + dmac[5]); + portduino_config.mac_address = macBuf; + found_hat = true; + } + } else { std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat" << std::endl; } @@ -278,7 +317,7 @@ void portduinoSetup() // RAK6421-13300-S1:aabbcc123456:5ba85807d92138b7519cfb60460573af:3061e8d8 // :mac address :<16 random unique bytes in hexidecimal> : crc32 // crc32 is calculated on the eeprom string up to but not including the final colon - if (strlen(autoconf_product) < 6) { + if (strlen(autoconf_product) < 6 && portduino_config.i2cdev != "") { try { char *mac_start = nullptr; char *devID_start = nullptr; @@ -363,8 +402,25 @@ void portduinoSetup() if (found_hat) { product_config = cleanupNameForAutoconf("lora-hat-" + std::string(hat_vendor) + "-" + autoconf_product + ".yaml"); + if (strncmp(hat_vendor, "RAK", strlen("RAK")) == 0 && + strncmp(autoconf_product, "6421 Pi Hat", strlen("6421 Pi Hat")) == 0) { + std::cout << "autoconf: Setting hardwareModel to RAK6421" << std::endl; + portduino_status.hardwareModel = meshtastic_HardwareModel_RAK6421; + } } else if (found_ch341) { product_config = cleanupNameForAutoconf("lora-usb-" + std::string(autoconf_product) + ".yaml"); + // look for more data after the null terminator + size_t len = strlen(autoconf_product); + if (len < 74) { + memcpy(portduino_config.device_id, autoconf_product + len + 1, 16); + if (!memfll(portduino_config.device_id, '\0', 16) && !memfll(portduino_config.device_id, 0xff, 16)) { + portduino_config.has_device_id = true; + if (strncmp(autoconf_product, "MESHSTICK 1262", strlen("MESHSTICK 1262")) == 0) { + std::cout << "autoconf: Setting hardwareModel to Meshstick 1262" << std::endl; + portduino_status.hardwareModel = meshtastic_HardwareModel_MESHSTICK_1262; + } + } + } } // Don't try to automatically find config for a device with RAK eeprom. @@ -410,9 +466,11 @@ void portduinoSetup() ch341Hal->getProductString(product_string, 95); std::cout << "CH341 Product " << product_string << std::endl; if (strlen(serial) == 8 && portduino_config.mac_address.length() < 12) { - uint8_t hash[32] = {0}; + std::cout << "Deriving MAC address from Serial and Product String" << std::endl; + uint8_t hash[104] = {0}; memcpy(hash, serial, 8); - crypto->hash(hash, 8); + memcpy(hash + 8, product_string, strlen(product_string)); + crypto->hash(hash, 8 + strlen(product_string)); dmac[0] = (hash[0] << 4) | 2; dmac[1] = hash[1]; dmac[2] = hash[2]; @@ -426,11 +484,13 @@ void portduinoSetup() } getMacAddr(dmac); +#ifndef UNIT_TEST if (dmac[0] == 0 && dmac[1] == 0 && dmac[2] == 0 && dmac[3] == 0 && dmac[4] == 0 && dmac[5] == 0) { std::cout << "*** Blank MAC Address not allowed!" << std::endl; std::cout << "Please set a MAC Address in config.yaml using either MACAddress or MACAddressSource." << std::endl; exit(EXIT_FAILURE); } +#endif printf("MAC ADDRESS: %02X:%02X:%02X:%02X:%02X:%02X\n", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]); // Rather important to set this, if not running simulated. randomSeed(time(NULL)); @@ -441,6 +501,11 @@ void portduinoSetup() max_GPIO = i->pin; } + for (auto i : portduino_config.extra_pins) { + if (i.enabled && i.pin > max_GPIO) + max_GPIO = i.pin; + } + gpioInit(max_GPIO + 1); // Done here so we can inform Portduino how many GPIOs we need. // Need to bind all the configured GPIO pins so they're not simulated @@ -458,11 +523,25 @@ void portduinoSetup() } } } + for (auto i : portduino_config.extra_pins) { + // In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora + // Those GPIO are handled in our usermode driver instead. + if (i.config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") { + continue; + } + if (i.enabled) { + if (initGPIOPin(i.pin, gpioChipName + std::to_string(i.gpiochip), i.line) != ERRNO_OK) { + printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i.line); + exit(EXIT_FAILURE); + } + } + } // Only initialize the radio pins when dealing with real, kernel controlled SPI hardware if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") { SPI.begin(portduino_config.lora_spi_dev.c_str()); } + if (portduino_config.traceFilename != "") { try { traceFile.open(portduino_config.traceFilename, std::ios::out | std::ios::app); @@ -470,6 +549,21 @@ void portduinoSetup() std::cout << "*** traceFile Exception " << e.what() << std::endl; exit(EXIT_FAILURE); } + if (!traceFile.is_open()) { + std::cout << "*** traceFile open failure" << std::endl; + exit(EXIT_FAILURE); + } + } else if (portduino_config.JSONFilename != "") { + try { + JSONFile.open(portduino_config.JSONFilename, std::ios::out | std::ios::app); + } catch (std::ofstream::failure &e) { + std::cout << "*** JSONFile Exception " << e.what() << std::endl; + exit(EXIT_FAILURE); + } + if (!JSONFile.is_open()) { + std::cout << "*** JSONFile open failure" << std::endl; + exit(EXIT_FAILURE); + } } if (verboseEnabled && portduino_config.logoutputlevel != level_trace) { portduino_config.logoutputlevel = level_debug; @@ -517,6 +611,29 @@ bool loadConfig(const char *configPath) portduino_config.logoutputlevel = level_error; } portduino_config.traceFilename = yamlConfig["Logging"]["TraceFile"].as(""); + portduino_config.JSONFilename = yamlConfig["Logging"]["JSONFile"].as(""); + portduino_config.JSONFilter = (_meshtastic_PortNum)yamlConfig["Logging"]["JSONFilter"].as(0); + if (yamlConfig["Logging"]["JSONFilter"].as("") == "textmessage") + portduino_config.JSONFilter = meshtastic_PortNum_TEXT_MESSAGE_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "telemetry") + portduino_config.JSONFilter = meshtastic_PortNum_TELEMETRY_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "nodeinfo") + portduino_config.JSONFilter = meshtastic_PortNum_NODEINFO_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "position") + portduino_config.JSONFilter = meshtastic_PortNum_POSITION_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "waypoint") + portduino_config.JSONFilter = meshtastic_PortNum_WAYPOINT_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "neighborinfo") + portduino_config.JSONFilter = meshtastic_PortNum_NEIGHBORINFO_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "traceroute") + portduino_config.JSONFilter = meshtastic_PortNum_TRACEROUTE_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "detection") + portduino_config.JSONFilter = meshtastic_PortNum_DETECTION_SENSOR_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "paxcounter") + portduino_config.JSONFilter = meshtastic_PortNum_PAXCOUNTER_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "remotehardware") + portduino_config.JSONFilter = meshtastic_PortNum_REMOTE_HARDWARE_APP; + if (yamlConfig["Logging"]["AsciiLogs"]) { // Default is !isatty(1) but can be set explicitly in config.yaml portduino_config.ascii_logs = yamlConfig["Logging"]["AsciiLogs"].as(); @@ -544,6 +661,19 @@ bool loadConfig(const char *configPath) if (yamlConfig["Lora"]["RF95_MAX_POWER"]) portduino_config.rf95_max_power = yamlConfig["Lora"]["RF95_MAX_POWER"].as(20); + if (yamlConfig["Lora"]["TX_GAIN_LORA"]) { + YAML::Node tx_gain_node = yamlConfig["Lora"]["TX_GAIN_LORA"]; + if (tx_gain_node.IsSequence() && tx_gain_node.size() != 0) { + portduino_config.num_pa_points = min(tx_gain_node.size(), std::size(portduino_config.tx_gain_lora)); + for (int i = 0; i < portduino_config.num_pa_points; i++) { + portduino_config.tx_gain_lora[i] = tx_gain_node[i].as(); + } + } else { + portduino_config.num_pa_points = 1; + portduino_config.tx_gain_lora[0] = tx_gain_node.as(0); + } + } + if (portduino_config.lora_module != use_autoconf && portduino_config.lora_module != use_simradio && !portduino_config.force_simradio) { portduino_config.dio2_as_rf_switch = yamlConfig["Lora"]["DIO2_AS_RF_SWITCH"].as(false); @@ -632,6 +762,16 @@ bool loadConfig(const char *configPath) portduino_config.has_gps = 1; } } + if (yamlConfig["GPIO"]["ExtraPins"]) { + for (auto extra_pin : yamlConfig["GPIO"]["ExtraPins"]) { + portduino_config.extra_pins.push_back(pinMapping()); + portduino_config.extra_pins.back().config_section = "GPIO"; + portduino_config.extra_pins.back().config_name = "ExtraPins"; + portduino_config.extra_pins.back().enabled = true; + readGPIOFromYaml(extra_pin, portduino_config.extra_pins.back()); + } + } + if (yamlConfig["I2C"]) { portduino_config.i2cdev = yamlConfig["I2C"]["I2CDevice"].as(""); } @@ -732,6 +872,7 @@ bool loadConfig(const char *configPath) } if (yamlConfig["Config"]) { + portduino_config.has_config_overrides = true; if (yamlConfig["Config"]["DisplayMode"]) { portduino_config.has_configDisplayMode = true; if ((yamlConfig["Config"]["DisplayMode"]).as("") == "TWOCOLOR") { @@ -744,6 +885,13 @@ bool loadConfig(const char *configPath) portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; } } + if (yamlConfig["Config"]["StatusMessage"]) { + portduino_config.has_statusMessage = true; + portduino_config.statusMessage = (yamlConfig["Config"]["StatusMessage"]).as(""); + } + if ((yamlConfig["Config"]["EnableUDP"]).as(false)) { + portduino_config.enable_UDP = true; + } } if (yamlConfig["General"]) { @@ -757,6 +905,12 @@ bool loadConfig(const char *configPath) std::cout << "Cannot set both MACAddress and MACAddressSource!" << std::endl; exit(EXIT_FAILURE); } + if (checkConfigPort) { + portduino_config.api_port = (yamlConfig["General"]["APIPort"]).as(-1); + if (portduino_config.api_port != -1 && portduino_config.api_port > 1023 && portduino_config.api_port < 65536) { + TCPPort = (portduino_config.api_port); + } + } portduino_config.mac_address = (yamlConfig["General"]["MACAddress"]).as(""); if (portduino_config.mac_address != "") { portduino_config.mac_address_explicit = true; @@ -827,4 +981,4 @@ void readGPIOFromYaml(YAML::Node sourceNode, pinMapping &destPin, int pinDefault destPin.line = destPin.pin; destPin.gpiochip = portduino_config.lora_default_gpiochip; } -} \ No newline at end of file +} diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index 3fe017d5e..990803e79 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -1,13 +1,22 @@ #pragma once #include #include +#include #include +#include #include "LR11x0Interface.h" #include "Module.h" -#include "platform/portduino/USBHal.h" +#include "mesh/generated/meshtastic/mesh.pb.h" #include "yaml-cpp/yaml.h" +extern struct portduino_status_struct { + bool LoRa_in_error = false; + _meshtastic_HardwareModel hardwareModel = meshtastic_HardwareModel_PORTDUINO; +} portduino_status; + +#include "platform/portduino/USBHal.h" + // Product strings for auto-configuration // {"PRODUCT_STRING", "CONFIG.YAML"} // YAML paths are relative to `meshtastic/available.d` @@ -46,6 +55,8 @@ struct pinMapping { }; extern std::ofstream traceFile; +extern std::ofstream JSONFile; + extern Ch341Hal *ch341Hal; int initGPIOPin(int pinNum, std::string gpioChipname, int line); bool loadConfig(const char *configPath); @@ -87,6 +98,8 @@ extern struct portduino_config_struct { int lora_usb_pid = 0x5512; int lora_usb_vid = 0x1A86; int spiSpeed = 2000000; + int num_pa_points = 1; // default to 1 point, with 0 gain + uint16_t tx_gain_lora[22] = {0}; pinMapping lora_cs_pin = {"Lora", "CS"}; pinMapping lora_irq_pin = {"Lora", "IRQ"}; pinMapping lora_busy_pin = {"Lora", "Busy"}; @@ -94,6 +107,7 @@ extern struct portduino_config_struct { pinMapping lora_txen_pin = {"Lora", "TXen"}; pinMapping lora_rxen_pin = {"Lora", "RXen"}; pinMapping lora_sx126x_ant_sw_pin = {"Lora", "SX126X_ANT_SW"}; + std::vector extra_pins = {}; // GPS bool has_gps = false; @@ -148,6 +162,9 @@ extern struct portduino_config_struct { bool ascii_logs = !isatty(1); bool ascii_logs_explicit = false; + std::string JSONFilename; + meshtastic_PortNum JSONFilter = (_meshtastic_PortNum)0; + // Webserver std::string webserver_root_path = ""; std::string webserver_ssl_key_path = "/etc/meshtasticd/ssl/private_key.pem"; @@ -160,13 +177,18 @@ extern struct portduino_config_struct { int hostMetrics_channel = 0; // config + bool has_config_overrides = false; int configDisplayMode = 0; bool has_configDisplayMode = false; + std::string statusMessage = ""; + bool has_statusMessage = false; + bool enable_UDP = false; // General std::string mac_address = ""; bool mac_address_explicit = false; std::string mac_address_source = ""; + int api_port = -1; std::string config_directory = ""; std::string available_directory = "/etc/meshtasticd/available.d/"; int maxtophone = 100; @@ -222,6 +244,17 @@ extern struct portduino_config_struct { out << YAML::Key << "LR1120_MAX_POWER" << YAML::Value << lr1120_max_power; if (rf95_max_power != 20) out << YAML::Key << "RF95_MAX_POWER" << YAML::Value << rf95_max_power; + + if (num_pa_points > 1) { + out << YAML::Key << "TX_GAIN_LORA" << YAML::Value << YAML::Flow << YAML::BeginSeq; + for (int i = 0; i < num_pa_points; i++) { + out << YAML::Value << tx_gain_lora[i]; + } + out << YAML::EndSeq; + } else if (tx_gain_lora[0] != 0) { + out << YAML::Key << "TX_GAIN_LORA" << YAML::Value << tx_gain_lora[0]; + } + out << YAML::Key << "DIO2_AS_RF_SWITCH" << YAML::Value << dio2_as_rf_switch; if (dio3_tcxo_voltage != 0) out << YAML::Key << "DIO3_TCXO_VOLTAGE" << YAML::Value << YAML::Precision(3) << (float)dio3_tcxo_voltage / 1000; @@ -294,6 +327,20 @@ extern struct portduino_config_struct { } out << YAML::EndMap; // Lora + if (!extra_pins.empty()) { + out << YAML::Key << "GPIO" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "ExtraPins" << YAML::Value << YAML::BeginSeq; + for (auto extra : extra_pins) { + out << YAML::BeginMap; + out << YAML::Key << "pin" << YAML::Value << extra.pin; + out << YAML::Key << "line" << YAML::Value << extra.line; + out << YAML::Key << "gpiochip" << YAML::Value << extra.gpiochip; + out << YAML::EndMap; + } + out << YAML::EndSeq; + out << YAML::EndMap; // GPIO + } + if (i2cdev != "") { out << YAML::Key << "I2C" << YAML::Value << YAML::BeginMap; out << YAML::Key << "I2CDevice" << YAML::Value << i2cdev; @@ -413,6 +460,29 @@ extern struct portduino_config_struct { } if (traceFilename != "") out << YAML::Key << "TraceFile" << YAML::Value << traceFilename; + if (JSONFilename != "") { + out << YAML::Key << "JSONFile" << YAML::Value << JSONFilename; + if (JSONFilter == meshtastic_PortNum_TEXT_MESSAGE_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "textmessage"; + else if (JSONFilter == meshtastic_PortNum_TELEMETRY_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "telemetry"; + else if (JSONFilter == meshtastic_PortNum_NODEINFO_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "nodeinfo"; + else if (JSONFilter == meshtastic_PortNum_POSITION_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "position"; + else if (JSONFilter == meshtastic_PortNum_WAYPOINT_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "waypoint"; + else if (JSONFilter == meshtastic_PortNum_NEIGHBORINFO_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "neighborinfo"; + else if (JSONFilter == meshtastic_PortNum_TRACEROUTE_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "traceroute"; + else if (JSONFilter == meshtastic_PortNum_DETECTION_SENSOR_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "detection"; + else if (JSONFilter == meshtastic_PortNum_PAXCOUNTER_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "paxcounter"; + else if (JSONFilter == meshtastic_PortNum_REMOTE_HARDWARE_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "remotehardware"; + } if (ascii_logs_explicit) { out << YAML::Key << "AsciiLogs" << YAML::Value << ascii_logs; } @@ -439,21 +509,30 @@ extern struct portduino_config_struct { } // config - if (has_configDisplayMode) { + if (has_config_overrides) { out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap; - switch (configDisplayMode) { - case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR: - out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR"; - break; - case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED: - out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED"; - break; - case meshtastic_Config_DisplayConfig_DisplayMode_COLOR: - out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR"; - break; - case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT: - out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT"; - break; + if (has_configDisplayMode) { + + switch (configDisplayMode) { + case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR: + out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR"; + break; + case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED: + out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED"; + break; + case meshtastic_Config_DisplayConfig_DisplayMode_COLOR: + out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR"; + break; + case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT: + out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT"; + break; + } + } + if (has_statusMessage) { + out << YAML::Key << "StatusMessage" << YAML::Value << statusMessage; + } + if (enable_UDP) { + out << YAML::Key << "EnableUDP" << YAML::Value << true; } out << YAML::EndMap; // Config @@ -463,6 +542,8 @@ extern struct portduino_config_struct { out << YAML::Key << "General" << YAML::Value << YAML::BeginMap; if (config_directory != "") out << YAML::Key << "ConfigDirectory" << YAML::Value << config_directory; + if (api_port != -1) + out << YAML::Key << "TCPPort" << YAML::Value << api_port; if (mac_address_explicit) out << YAML::Key << "MACAddress" << YAML::Value << mac_address; if (mac_address_source != "") @@ -474,4 +555,4 @@ extern struct portduino_config_struct { out << YAML::EndMap; // General return out.c_str(); } -} portduino_config; \ No newline at end of file +} portduino_config; diff --git a/src/platform/portduino/USBHal.h b/src/platform/portduino/USBHal.h index ce2a5cfd3..441f75b10 100644 --- a/src/platform/portduino/USBHal.h +++ b/src/platform/portduino/USBHal.h @@ -9,6 +9,8 @@ #include #include +extern uint32_t rebootAtMsec; + // include the library for Raspberry GPIO pins #define PI_RISING (PINEDIO_INT_MODE_RISING) @@ -45,7 +47,7 @@ class Ch341Hal : public RadioLibHal int32_t ret = pinedio_init(&pinedio, NULL); if (ret != 0) { std::string s = "Could not open SPI: "; - throw(s + std::to_string(ret)); + throw std::runtime_error(s + std::to_string(ret)); } pinedio_set_option(&pinedio, PINEDIO_OPTION_AUTO_CS, 0); @@ -64,7 +66,7 @@ class Ch341Hal : public RadioLibHal void getProductString(char *_product_string, size_t len) { len = len > 95 ? 95 : len; - strncpy(_product_string, pinedio.product_string, len); + memcpy(_product_string, pinedio.product_string, len); } void init() override {} @@ -74,30 +76,55 @@ class Ch341Hal : public RadioLibHal // RADIOLIB_NC as an alias for non-connected pins void pinMode(uint32_t pin, uint32_t mode) override { + if (checkError()) { + return; + } if (pin == RADIOLIB_NC) { return; } - pinedio_set_pin_mode(&pinedio, pin, mode); + auto res = pinedio_set_pin_mode(&pinedio, pin, mode); + if (res < 0 && rebootAtMsec == 0) { + LOG_ERROR("USBHal pinMode: Could not set pin %u mode to %u: %d", pin, mode, res); + } } void digitalWrite(uint32_t pin, uint32_t value) override { + if (checkError()) { + return; + } if (pin == RADIOLIB_NC) { return; } - pinedio_digital_write(&pinedio, pin, value); + auto res = pinedio_digital_write(&pinedio, pin, value); + if (res < 0 && rebootAtMsec == 0) { + LOG_ERROR("USBHal digitalWrite: Could not write pin %u: %d", pin, res); + portduino_status.LoRa_in_error = true; + } } uint32_t digitalRead(uint32_t pin) override { + if (checkError()) { + return 0; + } if (pin == RADIOLIB_NC) { return 0; } - return pinedio_digital_read(&pinedio, pin); + auto res = pinedio_digital_read(&pinedio, pin); + if (res < 0 && rebootAtMsec == 0) { + LOG_ERROR("USBHal digitalRead: Could not read pin %u: %d", pin, res); + portduino_status.LoRa_in_error = true; + return 0; + } + return res; } void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override { + if (checkError()) { + return; + } if (interruptNum == RADIOLIB_NC) { return; } @@ -107,6 +134,9 @@ class Ch341Hal : public RadioLibHal void detachInterrupt(uint32_t interruptNum) override { + if (checkError()) { + return; + } if (interruptNum == RADIOLIB_NC) { return; } @@ -152,6 +182,9 @@ class Ch341Hal : public RadioLibHal void spiTransfer(uint8_t *out, size_t len, uint8_t *in) { + if (checkError()) { + return; + } int32_t ret = pinedio_transceive(&this->pinedio, out, in, len); if (ret < 0) { std::cerr << "Could not perform SPI transfer: " << ret << std::endl; @@ -160,9 +193,22 @@ class Ch341Hal : public RadioLibHal void spiEndTransaction() {} void spiEnd() {} + bool checkError() + { + if (pinedio.in_error) { + if (!has_warned) + LOG_ERROR("USBHal: libch341 in_error detected"); + portduino_status.LoRa_in_error = true; + has_warned = true; + return true; + } + has_warned = false; + return false; + } private: pinedio_inst pinedio = {0}; + bool has_warned = false; }; #endif diff --git a/src/platform/portduino/architecture.h b/src/platform/portduino/architecture.h index e10519d21..b1698a4eb 100644 --- a/src/platform/portduino/architecture.h +++ b/src/platform/portduino/architecture.h @@ -6,7 +6,7 @@ // set HW_VENDOR // -#define HW_VENDOR meshtastic_HardwareModel_PORTDUINO +#define HW_VENDOR portduino_status.hardwareModel #ifndef HAS_BUTTON #define HAS_BUTTON 1 @@ -17,9 +17,6 @@ #ifndef HAS_RADIO #define HAS_RADIO 1 #endif -#ifndef HAS_RTC -#define HAS_RTC 1 -#endif #ifndef HAS_TELEMETRY #define HAS_TELEMETRY 1 #endif diff --git a/src/power.h b/src/power.h index c826d98b4..e4b456d3b 100644 --- a/src/power.h +++ b/src/power.h @@ -15,20 +15,8 @@ // Device specific curves go in variant.h #ifndef OCV_ARRAY -#ifdef CELL_TYPE_LIFEPO4 -#define OCV_ARRAY 3400, 3350, 3320, 3290, 3270, 3260, 3250, 3230, 3200, 3120, 3000 -#elif defined(CELL_TYPE_LEADACID) -#define OCV_ARRAY 2120, 2090, 2070, 2050, 2030, 2010, 1990, 1980, 1970, 1960, 1950 -#elif defined(CELL_TYPE_ALKALINE) -#define OCV_ARRAY 1580, 1400, 1350, 1300, 1280, 1250, 1230, 1190, 1150, 1100, 1000 -#elif defined(CELL_TYPE_NIMH) -#define OCV_ARRAY 1400, 1300, 1280, 1270, 1260, 1250, 1240, 1230, 1210, 1150, 1000 -#elif defined(CELL_TYPE_LTO) -#define OCV_ARRAY 2700, 2560, 2540, 2520, 2500, 2460, 2420, 2400, 2380, 2320, 1500 -#else // LiIon #define OCV_ARRAY 4190, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100 #endif -#endif /*Note: 12V lead acid is 6 cells, most board accept only 1 cell LiIon/LiPo*/ #ifndef NUM_CELLS @@ -121,6 +109,8 @@ class Power : private concurrency::OSThread bool lipoChargerInit(); /// Setup a meshSolar battery sensor bool meshSolarInit(); + /// Setup a serial battery sensor + bool serialBatteryInit(); private: void shutdown(); diff --git a/src/power/PowerHAL.cpp b/src/power/PowerHAL.cpp new file mode 100644 index 000000000..0a8d5f10b --- /dev/null +++ b/src/power/PowerHAL.cpp @@ -0,0 +1,19 @@ + +#include "PowerHAL.h" + +void powerHAL_init() +{ + return powerHAL_platformInit(); +} + +__attribute__((weak, noinline)) void powerHAL_platformInit() {} + +__attribute__((weak, noinline)) bool powerHAL_isPowerLevelSafe() +{ + return true; +} + +__attribute__((weak, noinline)) bool powerHAL_isVBUSConnected() +{ + return false; +} diff --git a/src/power/PowerHAL.h b/src/power/PowerHAL.h new file mode 100644 index 000000000..318b06810 --- /dev/null +++ b/src/power/PowerHAL.h @@ -0,0 +1,26 @@ + +/* + +Power Hardware Abstraction Layer. Set of API calls to offload power management, measurements, reboots, etc +to the platform and variant code to avoid #ifdef spaghetti hell and limitless device-based edge cases +in the main firmware code + +Functions declared here (with exception of powerHAL_init) should be defined in platform specific codebase. +Default function body does usually nothing. + +*/ + +// Initialize HAL layer. Call it as early as possible during device boot +// do not overwrite it as it's not declared with "weak" attribute. +void powerHAL_init(); + +// platform specific init code if needed to be run early on boot +void powerHAL_platformInit(); + +// Return true if current battery level is safe for device operation (for example flash writes). +// This should be reported by power failure comparator (NRF52) or similar circuits on other platforms. +// Do not use battery ADC as improper ADC configuration may prevent device from booting. +bool powerHAL_isPowerLevelSafe(); + +// return if USB voltage is connected +bool powerHAL_isVBUSConnected(); diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index b31d2dc2e..a12972cb0 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -418,8 +418,9 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi); if (mp->rx_snr != 0) jsonObj["snr"] = new JSONValue((float)mp->rx_snr); - if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { - jsonObj["hops_away"] = new JSONValue((unsigned int)(mp->hop_start - mp->hop_limit)); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); } @@ -450,8 +451,9 @@ std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPa jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi); if (mp->rx_snr != 0) jsonObj["snr"] = new JSONValue((float)mp->rx_snr); - if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { - jsonObj["hops_away"] = new JSONValue((unsigned int)(mp->hop_start - mp->hop_limit)); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); } jsonObj["size"] = new JSONValue((unsigned int)mp->encrypted.size); diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp index 353c710a1..41f505b94 100644 --- a/src/serialization/MeshPacketSerializer_nRF52.cpp +++ b/src/serialization/MeshPacketSerializer_nRF52.cpp @@ -358,8 +358,9 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, jsonObj["rssi"] = (int)mp->rx_rssi; if (mp->rx_snr != 0) jsonObj["snr"] = (float)mp->rx_snr; - if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { - jsonObj["hops_away"] = (unsigned int)(mp->hop_start - mp->hop_limit); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = (unsigned int)(hopsAway); jsonObj["hop_start"] = (unsigned int)(mp->hop_start); } @@ -393,8 +394,9 @@ std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPa jsonObj["rssi"] = (int)mp->rx_rssi; if (mp->rx_snr != 0) jsonObj["snr"] = (float)mp->rx_snr; - if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { - jsonObj["hops_away"] = (unsigned int)(mp->hop_start - mp->hop_limit); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = (unsigned int)(hopsAway); jsonObj["hop_start"] = (unsigned int)(mp->hop_start); } jsonObj["size"] = (unsigned int)mp->encrypted.size; diff --git a/src/sleep.cpp b/src/sleep.cpp index 756582c74..8b30a5352 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -241,7 +241,6 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN #ifdef PIN_POWER_EN digitalWrite(PIN_POWER_EN, LOW); pinMode(PIN_POWER_EN, INPUT); // power off peripherals - // pinMode(PIN_POWER_EN1, INPUT_PULLDOWN); #endif #ifdef RAK_WISMESH_TAP_V2 diff --git a/src/watchdog/watchdogThread.cpp b/src/watchdog/watchdogThread.cpp new file mode 100644 index 000000000..3e8a5466f --- /dev/null +++ b/src/watchdog/watchdogThread.cpp @@ -0,0 +1,37 @@ +#include "watchdogThread.h" +#include "configuration.h" + +#ifdef HAS_HARDWARE_WATCHDOG +WatchdogThread *watchdogThread; + +WatchdogThread::WatchdogThread() : OSThread("Watchdog") +{ + setup(); +} + +void WatchdogThread::feedDog(void) +{ + digitalWrite(HARDWARE_WATCHDOG_DONE, HIGH); + delay(1); + digitalWrite(HARDWARE_WATCHDOG_DONE, LOW); +} + +int32_t WatchdogThread::runOnce() +{ + LOG_DEBUG("Feeding hardware watchdog"); + feedDog(); + return HARDWARE_WATCHDOG_TIMEOUT_MS; +} + +bool WatchdogThread::setup() +{ + LOG_DEBUG("init hardware watchdog"); + pinMode(HARDWARE_WATCHDOG_WAKE, INPUT); + pinMode(HARDWARE_WATCHDOG_DONE, OUTPUT); + delay(1); + digitalWrite(HARDWARE_WATCHDOG_DONE, LOW); + delay(1); + feedDog(); + return true; +} +#endif \ No newline at end of file diff --git a/src/watchdog/watchdogThread.h b/src/watchdog/watchdogThread.h new file mode 100644 index 000000000..3a3830aa4 --- /dev/null +++ b/src/watchdog/watchdogThread.h @@ -0,0 +1,17 @@ +#pragma once + +#include "concurrency/OSThread.h" +#include + +#ifdef HAS_HARDWARE_WATCHDOG +class WatchdogThread : private concurrency::OSThread +{ + public: + WatchdogThread(); + void feedDog(void); + virtual bool setup(); + virtual int32_t runOnce() override; +}; + +extern WatchdogThread *watchdogThread; +#endif diff --git a/test/test_mqtt/MQTT.cpp b/test/test_mqtt/MQTT.cpp index 1c2f0642a..a566dabf7 100644 --- a/test/test_mqtt/MQTT.cpp +++ b/test/test_mqtt/MQTT.cpp @@ -605,12 +605,13 @@ void test_receiveAcksOwnSentMessages(void) unitTest->publish(&p, nodeDB->getNodeId().c_str()); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); - TEST_ASSERT_EQUAL(1, mockRoutingModule->ackNacks_.size()); - const auto &[err, to, idFrom, chIndex, hopLimit] = mockRoutingModule->ackNacks_.front(); - TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NONE, err); - TEST_ASSERT_EQUAL(myNodeInfo.my_node_num, to); - TEST_ASSERT_EQUAL(p.id, idFrom); + // FIXME: Better assertion for this test + // TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + // TEST_ASSERT_EQUAL(1, mockRoutingModule->ackNacks_.size()); + // const auto &[err, to, idFrom, chIndex, hopLimit] = mockRoutingModule->ackNacks_.front(); + // TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NONE, err); + // TEST_ASSERT_EQUAL(myNodeInfo.my_node_num, to); + // TEST_ASSERT_EQUAL(p.id, idFrom); } // Should ignore our own messages from MQTT that were heard by other nodes. diff --git a/test/test_radio/test_main.cpp b/test/test_radio/test_main.cpp new file mode 100644 index 000000000..fbe2b1b13 --- /dev/null +++ b/test/test_radio/test_main.cpp @@ -0,0 +1,100 @@ +#include "MeshRadio.h" +#include "RadioInterface.h" +#include "TestUtil.h" +#include + +#include "meshtastic/config.pb.h" + +static void test_bwCodeToKHz_specialMappings() +{ + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 31.25f, bwCodeToKHz(31)); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 62.5f, bwCodeToKHz(62)); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 203.125f, bwCodeToKHz(200)); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 406.25f, bwCodeToKHz(400)); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 812.5f, bwCodeToKHz(800)); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 1625.0f, bwCodeToKHz(1600)); +} + +static void test_bwCodeToKHz_passthrough() +{ + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 125.0f, bwCodeToKHz(125)); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 250.0f, bwCodeToKHz(250)); +} + +static void test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.use_preset = false; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; + cfg.bandwidth = 123; + cfg.spread_factor = 8; + + RadioInterface::bootstrapLoRaConfigFromPreset(cfg); + + TEST_ASSERT_EQUAL_UINT16(123, cfg.bandwidth); + TEST_ASSERT_EQUAL_UINT32(8, cfg.spread_factor); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset); +} + +static void test_bootstrapLoRaConfigFromPreset_setsDerivedFields_nonWideRegion() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.use_preset = true; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; + + RadioInterface::bootstrapLoRaConfigFromPreset(cfg); + + TEST_ASSERT_EQUAL_UINT16(250, cfg.bandwidth); + TEST_ASSERT_EQUAL_UINT32(9, cfg.spread_factor); +} + +static void test_bootstrapLoRaConfigFromPreset_setsDerivedFields_wideRegion() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.use_preset = true; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; + + RadioInterface::bootstrapLoRaConfigFromPreset(cfg); + + TEST_ASSERT_EQUAL_UINT16(800, cfg.bandwidth); + TEST_ASSERT_EQUAL_UINT32(9, cfg.spread_factor); +} + +static void test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegionSpan() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.use_preset = true; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; + + RadioInterface::bootstrapLoRaConfigFromPreset(cfg); + + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset); + TEST_ASSERT_EQUAL_UINT16(250, cfg.bandwidth); + TEST_ASSERT_EQUAL_UINT32(11, cfg.spread_factor); +} + +void setUp(void) {} +void tearDown(void) {} + +void setup() +{ + delay(10); + delay(2000); + + initializeTestEnvironment(); + + UNITY_BEGIN(); + RUN_TEST(test_bwCodeToKHz_specialMappings); + RUN_TEST(test_bwCodeToKHz_passthrough); + RUN_TEST(test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse); + RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_nonWideRegion); + RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_wideRegion); + RUN_TEST(test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegionSpan); + exit(UNITY_END()); +} + +void loop() {} diff --git a/userPrefs.jsonc b/userPrefs.jsonc index 0c92eabcf..9e916aae2 100644 --- a/userPrefs.jsonc +++ b/userPrefs.jsonc @@ -55,6 +55,7 @@ // "USERPREFS_MQTT_TLS_ENABLED": "false", // "USERPREFS_MQTT_ROOT_TOPIC": "event/REPLACEME", // "USERPREFS_RINGTONE_NAG_SECS": "60", + // "USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS": "43200", "USERPREFS_RINGTONE_RTTTL": "24:d=32,o=5,b=565:f6,p,f6,4p,p,f6,p,f6,2p,p,b6,p,b6,p,b6,p,b6,p,b,p,b,p,b,p,b,p,b,p,b,p,b,p,b,1p.,2p.,p", // "USERPREFS_NETWORK_IPV6_ENABLED": "1", "USERPREFS_TZ_STRING": "tzplaceholder " diff --git a/variants/esp32/betafpv_2400_tx_micro/platformio.ini b/variants/esp32/betafpv_2400_tx_micro/platformio.ini index 4d163d834..77a1f7043 100644 --- a/variants/esp32/betafpv_2400_tx_micro/platformio.ini +++ b/variants/esp32/betafpv_2400_tx_micro/platformio.ini @@ -15,4 +15,5 @@ upload_protocol = esptool upload_speed = 460800 lib_deps = ${esp32_base.lib_deps} - adafruit/Adafruit NeoPixel @ ^1.12.0 + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 diff --git a/variants/esp32/betafpv_900_tx_nano/platformio.ini b/variants/esp32/betafpv_900_tx_nano/platformio.ini index 7e01fd2fa..a4ebe9694 100644 --- a/variants/esp32/betafpv_900_tx_nano/platformio.ini +++ b/variants/esp32/betafpv_900_tx_nano/platformio.ini @@ -13,5 +13,3 @@ board_build.f_cpu = 240000000L upload_protocol = esptool ;upload_port = /dev/ttyUSB0 upload_speed = 460800 -lib_deps = - ${esp32_base.lib_deps} diff --git a/variants/esp32/chatter2/platformio.ini b/variants/esp32/chatter2/platformio.ini index bf496bf26..62d23b1e6 100644 --- a/variants/esp32/chatter2/platformio.ini +++ b/variants/esp32/chatter2/platformio.ini @@ -6,7 +6,11 @@ build_flags = ${esp32_base.build_flags} -D CHATTER_2 -I variants/esp32/chatter2 + -DMESHTASTIC_EXCLUDE_WEBSERVER=1 + -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 + -ULED_BUILTIN lib_deps = ${esp32_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 diff --git a/variants/esp32/chatter2/variant.h b/variants/esp32/chatter2/variant.h index 0c1ef6967..28ce64f91 100644 --- a/variants/esp32/chatter2/variant.h +++ b/variants/esp32/chatter2/variant.h @@ -79,7 +79,7 @@ // lower dB for lower voltage rnage #define ADC_MULTIPLIER 5.0 // VBATT---10k--pin34---2.5K---GND // Chatter2 uses 3 AAA cells -#define CELL_TYPE_ALKALINE +#define OCV_ARRAY 1580, 1400, 1350, 1300, 1280, 1250, 1230, 1190, 1150, 1100, 1000 #define NUM_CELLS 3 #undef EXT_PWR_DETECT @@ -98,7 +98,6 @@ #define KB_LOAD 21 // load values from the switch and store in shift register #define KB_CLK 22 // clock pin for serial data out #define KB_DATA 23 // data pin -#define CANNED_MESSAGE_MODULE_ENABLE 1 ///////////////////////////////////////////////////////////////////////////////// // // diff --git a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini index 809599212..2ddc5a2db 100644 --- a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini +++ b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini @@ -10,3 +10,6 @@ build_flags = -D EBYTE_E22 -D EBYTE_E22_900M30S ; Assume Tx power curve is identical to 900M30S as there is no documentation -I variants/esp32/diy/9m2ibr_aprs_lora_tracker +build_src_filter = + ${esp32_base.build_src_filter} + +<../variants/esp32/diy/9m2ibr_aprs_lora_tracker> \ No newline at end of file diff --git a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.cpp b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.cpp new file mode 100644 index 000000000..ef90d5a54 --- /dev/null +++ b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.cpp @@ -0,0 +1,8 @@ +#include "variant.h" +#include "Arduino.h" + +void earlyInitVariant() +{ + pinMode(USER_LED, OUTPUT); + digitalWrite(USER_LED, HIGH ^ LED_STATE_ON); +} \ No newline at end of file diff --git a/variants/esp32/diy/dr-dev/platformio.ini b/variants/esp32/diy/dr-dev/platformio.ini index 5461d27b3..a1022934d 100644 --- a/variants/esp32/diy/dr-dev/platformio.ini +++ b/variants/esp32/diy/dr-dev/platformio.ini @@ -1,7 +1,15 @@ ; Port to Disaster Radio's ESP32-v3 Dev Board [env:meshtastic-dr-dev] +custom_meshtastic_hw_model = 41 +custom_meshtastic_hw_model_slug = DR_DEV +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = false +custom_meshtastic_display_name = DR-DEV +custom_meshtastic_tags = DIY + extends = esp32_base board = esp32doit-devkit-v1 +board_level = extra board_upload.maximum_size = 4194304 board_upload.maximum_ram_size = 532480 build_flags = diff --git a/variants/esp32/diy/hydra/platformio.ini b/variants/esp32/diy/hydra/platformio.ini index a922ed874..f23224f0b 100644 --- a/variants/esp32/diy/hydra/platformio.ini +++ b/variants/esp32/diy/hydra/platformio.ini @@ -1,8 +1,17 @@ ; Hydra - Meshtastic DIY v1 hardware with some specific changes [env:hydra] +custom_meshtastic_hw_model = 39 +custom_meshtastic_hw_model_slug = HYDRA +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Hydra +custom_meshtastic_tags = DIY + extends = esp32_base board = esp32doit-devkit-v1 build_flags = ${esp32_base.build_flags} -D DIY_V1 -I variants/esp32/diy/hydra + -ULED_BUILTIN diff --git a/variants/esp32/diy/v1/platformio.ini b/variants/esp32/diy/v1/platformio.ini index bcbd57cfa..6be2bfd09 100644 --- a/variants/esp32/diy/v1/platformio.ini +++ b/variants/esp32/diy/v1/platformio.ini @@ -1,5 +1,14 @@ ; Meshtastic DIY v1 by Nano VHF Schematic based on ESP32-WROOM-32 (38 pins) devkit & EBYTE E22 SX1262/SX1268 module [env:meshtastic-diy-v1] +custom_meshtastic_hw_model = 39 +custom_meshtastic_hw_model_slug = DIY_V1 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = DIY V1 +custom_meshtastic_images = diy.svg +custom_meshtastic_tags = DIY + extends = esp32_base board = esp32doit-devkit-v1 board_check = true @@ -8,3 +17,4 @@ build_flags = -D DIY_V1 -D EBYTE_E22 -I variants/esp32/diy/v1 + -ULED_BUILTIN diff --git a/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini new file mode 100644 index 000000000..bbbcd3cbe --- /dev/null +++ b/variants/esp32/esp32-common.ini @@ -0,0 +1,86 @@ +; Common settings for ESP targets, mixin with extends = esp32_common +[esp32_common] +extends = arduino_base +custom_esp32_kind = +custom_mtjson_part = +platform = + # renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32 + platformio/espressif32@6.12.0 + +extra_scripts = + ${env.extra_scripts} + pre:extra_scripts/esp32_pre.py + extra_scripts/esp32_extra.py + +build_src_filter = + ${arduino_base.build_src_filter} - - - - - + +upload_speed = 921600 +debug_init_break = tbreak setup +monitor_filters = esp32_exception_decoder + +board_build.filesystem = littlefs + +# Remove -DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL for low level BLE logging. +# See library directory for BLE logging possible values: .pio/libdeps/tbeam/NimBLE-Arduino/src/log_common/log_common.h +# This overrides the BLE logging default of LOG_LEVEL_INFO (1) from: .pio/libdeps/tbeam/NimBLE-Arduino/src/esp_nimble_cfg.h +build_unflags = -fno-lto +build_flags = + ${arduino_base.build_flags} + -flto + -Wall + -Wextra + -Isrc/platform/esp32 + -std=c++11 + -DLOG_LOCAL_LEVEL=ESP_LOG_DEBUG + -DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG + -DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL + -DAXP_DEBUG_PORT=Serial + -DCONFIG_BT_NIMBLE_ENABLED + -DCONFIG_BT_NIMBLE_MAX_BONDS=6 # default is 3 + -DCONFIG_NIMBLE_CPP_LOG_LEVEL=2 + -DCONFIG_BT_NIMBLE_MAX_CCCDS=20 + -DCONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192 + -DESP_OPENSSL_SUPPRESS_LEGACY_WARNING + -DSERIAL_BUFFER_SIZE=4096 + -DSERIAL_HAS_ON_RECEIVE + -DLIBPAX_ARDUINO + -DLIBPAX_WIFI + -DLIBPAX_BLE + -DHAS_UDP_MULTICAST=1 + ;-DDEBUG_HEAP + -DCAN_RECLOCK_I2C + +lib_deps = + ${arduino_base.lib_deps} + ${networking_base.lib_deps} + ${networking_extra.lib_deps} + ${environmental_base.lib_deps} + ${environmental_extra.lib_deps} + ${radiolib_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master + https://github.com/meshtastic/esp32_https_server/archive/b0f3960b3e8444563280656d88e22b5899481884.zip + # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino + h2zero/NimBLE-Arduino@^1.4.3 + # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master + https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip + # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib + https://github.com/lewisxhe/XPowersLib/archive/v0.3.3.zip + # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto + rweather/Crypto@0.4.0 + +lib_ignore = + segger_rtt + ESP32 BLE Arduino + +; leave this commented out to avoid breaking Windows +;upload_port = /dev/ttyUSB0 +;monitor_port = /dev/ttyUSB0 + +; Please don't delete these lines. JM uses them. +;upload_port = /dev/cu.SLAB_USBtoUART +;monitor_port = /dev/cu.SLAB_USBtoUART + +; customize the partition table +; http://docs.platformio.org/en/latest/platforms/espressif32.html#partition-tables +board_build.partitions = partition-table.csv diff --git a/variants/esp32/esp32.ini b/variants/esp32/esp32.ini index 5171bc45c..20ce38fae 100644 --- a/variants/esp32/esp32.ini +++ b/variants/esp32/esp32.ini @@ -1,86 +1,28 @@ -; Common settings for ESP targes, mixin with extends = esp32_base +; Common settings for ESP32 OG (without suffix) +; See 'esp32_common' for common ESP32-family settings [esp32_base] -extends = arduino_base +extends = esp32_common custom_esp32_kind = esp32 -custom_mtjson_part = -platform = - # renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32 - platformio/espressif32@6.11.0 -extra_scripts = - ${env.extra_scripts} - pre:extra_scripts/esp32_pre.py - extra_scripts/esp32_extra.py - -build_src_filter = - ${arduino_base.build_src_filter} - - - - - - -upload_speed = 921600 -debug_init_break = tbreak setup -monitor_filters = esp32_exception_decoder - -board_build.filesystem = littlefs - -# Remove -DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL for low level BLE logging. -# See library directory for BLE logging possible values: .pio/libdeps/tbeam/NimBLE-Arduino/src/log_common/log_common.h -# This overrides the BLE logging default of LOG_LEVEL_INFO (1) from: .pio/libdeps/tbeam/NimBLE-Arduino/src/esp_nimble_cfg.h -build_unflags = -fno-lto build_flags = - ${arduino_base.build_flags} - -flto - -Wall - -Wextra - -Isrc/platform/esp32 - -std=c++11 - -DLOG_LOCAL_LEVEL=ESP_LOG_DEBUG - -DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG - -DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL - -DAXP_DEBUG_PORT=Serial - -DCONFIG_BT_NIMBLE_ENABLED - -DCONFIG_BT_NIMBLE_MAX_BONDS=6 # default is 3 - -DCONFIG_NIMBLE_CPP_LOG_LEVEL=2 - -DCONFIG_BT_NIMBLE_MAX_CCCDS=20 - -DCONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192 - -DESP_OPENSSL_SUPPRESS_LEGACY_WARNING - -DSERIAL_BUFFER_SIZE=4096 - -DSERIAL_HAS_ON_RECEIVE - -DLIBPAX_ARDUINO - -DLIBPAX_WIFI - -DLIBPAX_BLE - -DHAS_UDP_MULTICAST=1 - ;-DDEBUG_HEAP - + ${esp32_common.build_flags} + -DMESHTASTIC_EXCLUDE_AUDIO=1 +; Override lib_deps to use environmental_extra_no_bsec instead of environmental_extra +; BSEC library uses ~3.5KB DRAM which causes overflow on original ESP32 targets lib_deps = ${arduino_base.lib_deps} ${networking_base.lib_deps} + ${networking_extra.lib_deps} ${environmental_base.lib_deps} - ${environmental_extra.lib_deps} + ${environmental_extra_no_bsec.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master - https://github.com/meshtastic/esp32_https_server/archive/3223704846752e6d545139204837bdb2a55459ca.zip + https://github.com/meshtastic/esp32_https_server/archive/b0f3960b3e8444563280656d88e22b5899481884.zip # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino h2zero/NimBLE-Arduino@^1.4.3 # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib - https://github.com/lewisxhe/XPowersLib/archive/v0.3.2.zip - # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master - https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip + https://github.com/lewisxhe/XPowersLib/archive/v0.3.3.zip # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto - rweather/Crypto@0.4.0 - -lib_ignore = - segger_rtt - ESP32 BLE Arduino - -; leave this commented out to avoid breaking Windows -;upload_port = /dev/ttyUSB0 -;monitor_port = /dev/ttyUSB0 - -; Please don't delete these lines. JM uses them. -;upload_port = /dev/cu.SLAB_USBtoUART -;monitor_port = /dev/cu.SLAB_USBtoUART - -; customize the partition table -; http://docs.platformio.org/en/latest/platforms/espressif32.html#partition-tables -board_build.partitions = partition-table.csv + rweather/Crypto@0.4.0 \ No newline at end of file diff --git a/variants/esp32/heltec_v1/platformio.ini b/variants/esp32/heltec_v1/platformio.ini index 4be3ba655..770326427 100644 --- a/variants/esp32/heltec_v1/platformio.ini +++ b/variants/esp32/heltec_v1/platformio.ini @@ -1,4 +1,11 @@ [env:heltec-v1] +custom_meshtastic_hw_model = 11 +custom_meshtastic_hw_model_slug = HELTEC_V1 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = false +custom_meshtastic_display_name = Heltec V1 +custom_meshtastic_tags = Heltec + ;build_type = debug ; to make it possible to step through our jtag debugger extends = esp32_base board_level = extra diff --git a/variants/esp32/heltec_v2.1/platformio.ini b/variants/esp32/heltec_v2.1/platformio.ini index 4dcd9e583..1f7caa16f 100644 --- a/variants/esp32/heltec_v2.1/platformio.ini +++ b/variants/esp32/heltec_v2.1/platformio.ini @@ -1,4 +1,11 @@ [env:heltec-v2_1] +custom_meshtastic_hw_model = 10 +custom_meshtastic_hw_model_slug = HELTEC_V2_1 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = false +custom_meshtastic_display_name = Heltec V2.1 +custom_meshtastic_tags = Heltec + board_level = extra ;build_type = debug ; to make it possible to step through our jtag debugger extends = esp32_base diff --git a/variants/esp32/heltec_v2/platformio.ini b/variants/esp32/heltec_v2/platformio.ini index ed455616d..5f15fb321 100644 --- a/variants/esp32/heltec_v2/platformio.ini +++ b/variants/esp32/heltec_v2/platformio.ini @@ -1,4 +1,11 @@ [env:heltec-v2_0] +custom_meshtastic_hw_model = 5 +custom_meshtastic_hw_model_slug = HELTEC_V2_0 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = false +custom_meshtastic_display_name = Heltec V2.0 +custom_meshtastic_tags = Heltec + ;build_type = debug ; to make it possible to step through our jtag debugger board_level = extra extends = esp32_base diff --git a/variants/esp32/heltec_wireless_bridge/platformio.ini b/variants/esp32/heltec_wireless_bridge/platformio.ini index 93c3e3394..6f9de7a84 100644 --- a/variants/esp32/heltec_wireless_bridge/platformio.ini +++ b/variants/esp32/heltec_wireless_bridge/platformio.ini @@ -1,9 +1,9 @@ [env:heltec-wireless-bridge] -;build_type = debug ; to make it possible to step through our jtag debugger +;build_type = debug ; to make it possible to step through our jtag debugger extends = esp32_base board_level = extra board = heltec_wifi_lora_32 -build_flags = +build_flags = ${esp32_base.build_flags} -I variants/esp32/heltec_wireless_bridge -D HELTEC_WIRELESS_BRIDGE @@ -13,6 +13,7 @@ build_flags = -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D MESHTASTIC_EXCLUDE_DETECTIONSENSOR=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -D MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1 -D MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 -D MESHTASTIC_EXCLUDE_GPS=1 diff --git a/variants/esp32/m5stack_core/platformio.ini b/variants/esp32/m5stack_core/platformio.ini index a0443a918..8fbbae895 100644 --- a/variants/esp32/m5stack_core/platformio.ini +++ b/variants/esp32/m5stack_core/platformio.ini @@ -1,4 +1,12 @@ [env:m5stack-core] +custom_meshtastic_hw_model = 42 +custom_meshtastic_hw_model_slug = M5STACK +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = M5 Stack +custom_meshtastic_tags = M5Stack + extends = esp32_base board = m5stack-core-esp32 monitor_filters = esp32_exception_decoder @@ -8,6 +16,8 @@ build_flags = ${esp32_base.build_flags} -I variants/esp32/m5stack_core -DM5STACK + -DMESHTASTIC_EXCLUDE_WEBSERVER=1 + -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 -DUSER_SETUP_LOADED -DTFT_SDA_READ -DTFT_DRIVER=0x9341 @@ -20,9 +30,9 @@ build_flags = -DTFT_BL=32 -DSPI_FREQUENCY=40000000 -DSPI_READ_FREQUENCY=16000000 - -DDISABLE_ALL_LIBRARY_WARNINGS lib_ignore = m5stack-core lib_deps = ${esp32_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 diff --git a/variants/esp32/m5stack_coreink/platformio.ini b/variants/esp32/m5stack_coreink/platformio.ini index 1a00788e3..af1535f59 100644 --- a/variants/esp32/m5stack_coreink/platformio.ini +++ b/variants/esp32/m5stack_coreink/platformio.ini @@ -18,8 +18,10 @@ build_flags = -DM5STACK lib_deps = ${esp32_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.6 + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 lib_ignore = m5stack-coreink monitor_filters = esp32_exception_decoder diff --git a/variants/esp32/m5stack_coreink/variant.h b/variants/esp32/m5stack_coreink/variant.h index ecd93b7be..9bf45f2ff 100644 --- a/variants/esp32/m5stack_coreink/variant.h +++ b/variants/esp32/m5stack_coreink/variant.h @@ -13,10 +13,8 @@ #define LED_STATE_ON 1 // State when LED is lit #define LED_PIN 10 -#include "pcf8563.h" // PCF8563 RTC Module #define PCF8563_RTC 0x51 -#define HAS_RTC 1 // Wheel // Down 37 diff --git a/variants/esp32/nano-g1-explorer/platformio.ini b/variants/esp32/nano-g1-explorer/platformio.ini index 2ba1f49e9..16ecb99cc 100644 --- a/variants/esp32/nano-g1-explorer/platformio.ini +++ b/variants/esp32/nano-g1-explorer/platformio.ini @@ -1,10 +1,17 @@ ; The 1.0 release of the nano-g1-explorer board [env:nano-g1-explorer] +custom_meshtastic_hw_model = 17 +custom_meshtastic_hw_model_slug = NANO_G1_EXPLORER +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Nano G1 Explorer +custom_meshtastic_tags = B&Q + extends = esp32_base board = ttgo-t-beam -lib_deps = - ${esp32_base.lib_deps} build_flags = ${esp32_base.build_flags} -D NANO_G1_EXPLORER -I variants/esp32/nano-g1-explorer + -ULED_BUILTIN diff --git a/variants/esp32/nano-g1/platformio.ini b/variants/esp32/nano-g1/platformio.ini index be8227de2..724e008c7 100644 --- a/variants/esp32/nano-g1/platformio.ini +++ b/variants/esp32/nano-g1/platformio.ini @@ -1,10 +1,17 @@ ; The 1.0 release of the nano-g1 board [env:nano-g1] +custom_meshtastic_hw_model = 14 +custom_meshtastic_hw_model_slug = NANO_G1 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Nano G1 +custom_meshtastic_tags = B&Q + extends = esp32_base board = ttgo-t-beam -lib_deps = - ${esp32_base.lib_deps} build_flags = ${esp32_base.build_flags} -D NANO_G1 -I variants/esp32/nano-g1 + -ULED_BUILTIN diff --git a/variants/esp32/radiomaster_900_bandit/platformio.ini b/variants/esp32/radiomaster_900_bandit/platformio.ini index d9eb78a57..0012f49d3 100644 --- a/variants/esp32/radiomaster_900_bandit/platformio.ini +++ b/variants/esp32/radiomaster_900_bandit/platformio.ini @@ -9,8 +9,10 @@ build_flags = -DHAS_STK8XXX=1 -O2 -I variants/esp32/radiomaster_900_bandit + -ULED_BUILTIN board_build.f_cpu = 240000000L upload_protocol = esptool lib_deps = ${esp32_base.lib_deps} + # renovate: datasource=github-tags depName=STK8xxx-Accelerometer packageName=gjelsoe/STK8xxx-Accelerometer https://github.com/gjelsoe/STK8xxx-Accelerometer/archive/v0.1.1.zip diff --git a/variants/esp32/radiomaster_900_bandit_micro/platformio.ini b/variants/esp32/radiomaster_900_bandit_micro/platformio.ini index 36a45787b..e58d06f1e 100644 --- a/variants/esp32/radiomaster_900_bandit_micro/platformio.ini +++ b/variants/esp32/radiomaster_900_bandit_micro/platformio.ini @@ -13,7 +13,6 @@ build_flags = -DCONFIG_DISABLE_HAL_LOCKS=1 -O2 -I variants/esp32/radiomaster_900_bandit_nano + -ULED_BUILTIN board_build.f_cpu = 240000000L upload_protocol = esptool -lib_deps = - ${esp32_base.lib_deps} diff --git a/variants/esp32/radiomaster_900_bandit_nano/platformio.ini b/variants/esp32/radiomaster_900_bandit_nano/platformio.ini index 9a7fad83b..7b3d187bf 100644 --- a/variants/esp32/radiomaster_900_bandit_nano/platformio.ini +++ b/variants/esp32/radiomaster_900_bandit_nano/platformio.ini @@ -1,4 +1,12 @@ [env:radiomaster_900_bandit_nano] +custom_meshtastic_hw_model = 64 +custom_meshtastic_hw_model_slug = RADIOMASTER_900_BANDIT_NANO +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 2 +custom_meshtastic_display_name = RadioMaster 900 Bandit Nano +custom_meshtastic_tags = RadioMaster + extends = esp32_base board = esp32doit-devkit-v1 build_flags = @@ -8,7 +16,6 @@ build_flags = -DCONFIG_DISABLE_HAL_LOCKS=1 -O2 -I variants/esp32/radiomaster_900_bandit_nano + -ULED_BUILTIN board_build.f_cpu = 240000000L upload_protocol = esptool -lib_deps = - ${esp32_base.lib_deps} diff --git a/variants/esp32/rak11200/pins_arduino.h b/variants/esp32/rak11200/pins_arduino.h index f383d54a7..263fbd5a0 100644 --- a/variants/esp32/rak11200/pins_arduino.h +++ b/variants/esp32/rak11200/pins_arduino.h @@ -6,8 +6,6 @@ #define LED_GREEN 12 #define LED_BLUE 2 -#define LED_BUILTIN LED_GREEN - static const uint8_t TX = 1; static const uint8_t RX = 3; diff --git a/variants/esp32/rak11200/platformio.ini b/variants/esp32/rak11200/platformio.ini index 170e80b41..63821a092 100644 --- a/variants/esp32/rak11200/platformio.ini +++ b/variants/esp32/rak11200/platformio.ini @@ -1,4 +1,13 @@ [env:rak11200] +custom_meshtastic_hw_model = 13 +custom_meshtastic_hw_model_slug = RAK11200 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = RAK WisBlock 11200 +custom_meshtastic_images = rak11200.svg +custom_meshtastic_tags = RAK + extends = esp32_base board = wiscore_rak11200 board_level = pr diff --git a/variants/esp32/rak11200/variant.h b/variants/esp32/rak11200/variant.h index 01edb8b73..fe7d05676 100644 --- a/variants/esp32/rak11200/variant.h +++ b/variants/esp32/rak11200/variant.h @@ -6,8 +6,6 @@ #define LED_GREEN 12 #define LED_BLUE 2 -#define LED_BUILTIN LED_GREEN - static const uint8_t TX = 1; static const uint8_t RX = 3; diff --git a/variants/esp32/station-g1/platformio.ini b/variants/esp32/station-g1/platformio.ini index 693a41ae8..fad003b20 100644 --- a/variants/esp32/station-g1/platformio.ini +++ b/variants/esp32/station-g1/platformio.ini @@ -1,10 +1,17 @@ ; The 1.0 release of the nano-g1 board [env:station-g1] +custom_meshtastic_hw_model = 25 +custom_meshtastic_hw_model_slug = STATION_G1 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Station G1 +custom_meshtastic_tags = B&Q + extends = esp32_base board = ttgo-t-beam -lib_deps = - ${esp32_base.lib_deps} build_flags = ${esp32_base.build_flags} -D STATION_G1 -I variants/esp32/station-g1 + -ULED_BUILTIN diff --git a/variants/esp32/tbeam/platformio.ini b/variants/esp32/tbeam/platformio.ini index c635081ff..dbaccee8f 100644 --- a/variants/esp32/tbeam/platformio.ini +++ b/variants/esp32/tbeam/platformio.ini @@ -1,25 +1,36 @@ ; The 1.0 release of the TBEAM board [env:tbeam] +custom_meshtastic_hw_model = 4 +custom_meshtastic_hw_model_slug = TBEAM +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = LILYGO T-Beam +custom_meshtastic_images = tbeam.svg +custom_meshtastic_tags = LilyGo + extends = esp32_base board = ttgo-t-beam -board_level = pr + board_check = true -lib_deps = ${esp32_base.lib_deps} build_flags = ${esp32_base.build_flags} -D TBEAM_V10 -I variants/esp32/tbeam -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue + -ULED_BUILTIN upload_speed = 921600 [env:tbeam-displayshield] extends = env:tbeam - +board_level = extra build_flags = ${env:tbeam.build_flags} -D USE_ST7796 lib_deps = ${env:tbeam.lib_deps} - https://github.com/meshtastic/st7796/archive/refs/tags/1.0.5.zip ; display addon - lewisxhe/SensorLib@0.3.1 ; touchscreen addon \ No newline at end of file + # renovate: datasource=github-tags depName=meshtastic-st7796 packageName=meshtastic/st7796 + https://github.com/meshtastic/st7796/archive/1.0.5.zip + # renovate: datasource=custom.pio depName=lewisxhe-SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 diff --git a/variants/esp32/tbeam/variant.h b/variants/esp32/tbeam/variant.h index 2d144a888..2c1e61c49 100644 --- a/variants/esp32/tbeam/variant.h +++ b/variants/esp32/tbeam/variant.h @@ -57,7 +57,6 @@ #ifndef TOUCH_IRQ #define TOUCH_IRQ -1 #endif -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define USE_VIRTUAL_KEYBOARD 1 #define ST7796_NSS 25 diff --git a/variants/esp32/tbeam_v07/platformio.ini b/variants/esp32/tbeam_v07/platformio.ini index 1647d9fd7..e2763fdec 100644 --- a/variants/esp32/tbeam_v07/platformio.ini +++ b/variants/esp32/tbeam_v07/platformio.ini @@ -1,5 +1,12 @@ ; The original TBEAM board without the AXP power chip and a few other changes [env:tbeam0_7] +custom_meshtastic_hw_model = 6 +custom_meshtastic_hw_model_slug = TBEAM_V0P7 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = false +custom_meshtastic_display_name = LILYGO T-Beam V0.7 +custom_meshtastic_tags = LilyGo + board_level = extra extends = esp32_base board = ttgo-t-beam diff --git a/variants/esp32/tlora_v1/platformio.ini b/variants/esp32/tlora_v1/platformio.ini index 1d879b6b0..c45cc2ce9 100644 --- a/variants/esp32/tlora_v1/platformio.ini +++ b/variants/esp32/tlora_v1/platformio.ini @@ -1,4 +1,11 @@ [env:tlora-v1] +custom_meshtastic_hw_model = 2 +custom_meshtastic_hw_model_slug = TLORA_V1 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = false +custom_meshtastic_display_name = LILYGO T-LoRa V1 +custom_meshtastic_tags = LilyGo + board_level = extra extends = esp32_base board = ttgo-lora32-v1 diff --git a/variants/esp32/tlora_v2/platformio.ini b/variants/esp32/tlora_v2/platformio.ini index 4a710ee34..68358bfc3 100644 --- a/variants/esp32/tlora_v2/platformio.ini +++ b/variants/esp32/tlora_v2/platformio.ini @@ -1,4 +1,11 @@ [env:tlora-v2] +custom_meshtastic_hw_model = 1 +custom_meshtastic_hw_model_slug = TLORA_V2 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = false +custom_meshtastic_display_name = LILYGO T-LoRa V2 +custom_meshtastic_tags = LilyGo + board_level = extra extends = esp32_base board = ttgo-lora32-v1 diff --git a/variants/esp32/tlora_v2_1_16/platformio.ini b/variants/esp32/tlora_v2_1_16/platformio.ini index 8d5bdab9e..dfdbcb152 100644 --- a/variants/esp32/tlora_v2_1_16/platformio.ini +++ b/variants/esp32/tlora_v2_1_16/platformio.ini @@ -1,9 +1,18 @@ [env:tlora-v2-1-1_6] +custom_meshtastic_hw_model = 3 +custom_meshtastic_hw_model_slug = TLORA_V2_1_1P6 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = LILYGO T-LoRa V2.1-1.6 +custom_meshtastic_images = tlora-v2-1-1_6.svg +custom_meshtastic_tags = LilyGo + extends = esp32_base board = ttgo-lora32-v21 board_check = true build_flags = - ${esp32_base.build_flags} -D TLORA_V2_1_16 -I variants/esp32/tlora_v2_1_16 + ${esp32_base.build_flags} -D TLORA_V2_1_16 -I variants/esp32/tlora_v2_1_16 -ULED_BUILTIN upload_speed = 115200 [env:sugarcube] diff --git a/variants/esp32/tlora_v2_1_18/platformio.ini b/variants/esp32/tlora_v2_1_18/platformio.ini index 432117485..173a48692 100644 --- a/variants/esp32/tlora_v2_1_18/platformio.ini +++ b/variants/esp32/tlora_v2_1_18/platformio.ini @@ -1,4 +1,13 @@ [env:tlora-v2-1-1_8] +custom_meshtastic_hw_model = 15 +custom_meshtastic_hw_model_slug = TLORA_V2_1_1P8 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = LILYGO T-LoRa V2.1-1.8 +custom_meshtastic_images = tlora-v2-1-1_8.svg +custom_meshtastic_tags = LilyGo, 2.4GHz + extends = esp32_base board_level = extra board = ttgo-lora32-v21 diff --git a/variants/esp32/tlora_v3_3_0_tcxo/platformio.ini b/variants/esp32/tlora_v3_3_0_tcxo/platformio.ini index 1258fd8b7..38f14ffc5 100644 --- a/variants/esp32/tlora_v3_3_0_tcxo/platformio.ini +++ b/variants/esp32/tlora_v3_3_0_tcxo/platformio.ini @@ -6,4 +6,5 @@ build_flags = -D TLORA_V2_1_16 -I variants/esp32/tlora_v2_1_16 -D LORA_TCXO_GPIO=12 - -D BUTTON_PIN=0 \ No newline at end of file + -D BUTTON_PIN=0 + -ULED_BUILTIN \ No newline at end of file diff --git a/variants/esp32/trackerd/variant.h b/variants/esp32/trackerd/variant.h index c4dfb9e93..0996e85ac 100644 --- a/variants/esp32/trackerd/variant.h +++ b/variants/esp32/trackerd/variant.h @@ -10,7 +10,6 @@ #define LED_PIN 13 // 13 red, 2 blue, 15 red -// #define HAS_BUTTON 0 #define BUTTON_PIN 0 #define BUTTON_NEED_PULLUP diff --git a/variants/esp32/wiphone/platformio.ini b/variants/esp32/wiphone/platformio.ini index 5cce94b13..fbd77be75 100644 --- a/variants/esp32/wiphone/platformio.ini +++ b/variants/esp32/wiphone/platformio.ini @@ -10,6 +10,9 @@ build_flags = -I variants/esp32/wiphone lib_deps = ${esp32_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 - sparkfun/SX1509 IO Expander@^3.0.5 - pololu/APA102@^3.0.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 + # renovate: datasource=custom.pio depName=SX1509 IO Expander packageName=sparkfun/library/SX1509 IO Expander + sparkfun/SX1509 IO Expander@3.0.6 + # renovate: datasource=custom.pio depName=APA102 packageName=pololu/library/APA102 + pololu/APA102@3.0.0 diff --git a/variants/esp32/wiphone/variant.h b/variants/esp32/wiphone/variant.h index 619ac622a..5baeb3936 100644 --- a/variants/esp32/wiphone/variant.h +++ b/variants/esp32/wiphone/variant.h @@ -26,7 +26,6 @@ #undef GPS_TX_PIN #define NO_GPS 1 #define HAS_GPS 0 -#define NO_SCREEN #define HAS_SCREEN 0 // Default SPI1 will be mapped to the display diff --git a/variants/esp32c3/diy/esp32c3_super_mini/platformio.ini b/variants/esp32c3/diy/esp32c3_super_mini/platformio.ini index c87baa7bf..2dca8e1a9 100644 --- a/variants/esp32c3/diy/esp32c3_super_mini/platformio.ini +++ b/variants/esp32c3/diy/esp32c3_super_mini/platformio.ini @@ -4,7 +4,7 @@ extends = esp32c3_base board = esp32-c3-devkitm-1 build_flags = - ${esp32_base.build_flags} + ${esp32c3_base.build_flags} -D PRIVATE_HW -I variants/esp32c3/diy/esp32c3_super_mini -D ARDUINO_USB_MODE=1 diff --git a/variants/esp32c3/esp32c3.ini b/variants/esp32c3/esp32c3.ini index 2ba3036d0..e5f117ad7 100644 --- a/variants/esp32c3/esp32c3.ini +++ b/variants/esp32c3/esp32c3.ini @@ -1,6 +1,11 @@ [esp32c3_base] -extends = esp32_base +extends = esp32_common custom_esp32_kind = esp32c3 monitor_speed = 115200 monitor_filters = esp32_c3_exception_decoder + +lib_deps = + ${esp32_common.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master + https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip diff --git a/variants/esp32c3/hackerboxes_esp32c3_oled/platformio.ini b/variants/esp32c3/hackerboxes_esp32c3_oled/platformio.ini index 5a72b9d74..6e31af200 100644 --- a/variants/esp32c3/hackerboxes_esp32c3_oled/platformio.ini +++ b/variants/esp32c3/hackerboxes_esp32c3_oled/platformio.ini @@ -3,7 +3,7 @@ extends = esp32c3_base board = esp32-c3-devkitm-1 board_level = extra build_flags = - ${esp32_base.build_flags} + ${esp32c3_base.build_flags} -D PRIVATE_HW -D ARDUINO_USB_MODE=1 -D ARDUINO_USB_CDC_ON_BOOT=1 diff --git a/variants/esp32c3/heltec_esp32c3/platformio.ini b/variants/esp32c3/heltec_esp32c3/platformio.ini index 705e2e996..d087e4fd0 100644 --- a/variants/esp32c3/heltec_esp32c3/platformio.ini +++ b/variants/esp32c3/heltec_esp32c3/platformio.ini @@ -1,9 +1,18 @@ [env:heltec-ht62-esp32c3-sx1262] +custom_meshtastic_hw_model = 53 +custom_meshtastic_hw_model_slug = HELTEC_HT62 +custom_meshtastic_architecture = esp32-c3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec HT62 +custom_meshtastic_images = heltec-ht62-esp32c3-sx1262.svg +custom_meshtastic_tags = Heltec + extends = esp32c3_base board = esp32-c3-devkitm-1 board_level = pr build_flags = - ${esp32_base.build_flags} + ${esp32c3_base.build_flags} -D HELTEC_HT62 -I variants/esp32c3/heltec_esp32c3 monitor_speed = 115200 diff --git a/variants/esp32c3/heltec_hru_3601/platformio.ini b/variants/esp32c3/heltec_hru_3601/platformio.ini index b5ff63eae..8200b6e87 100644 --- a/variants/esp32c3/heltec_hru_3601/platformio.ini +++ b/variants/esp32c3/heltec_hru_3601/platformio.ini @@ -2,8 +2,9 @@ extends = esp32c3_base board = adafruit_qtpy_esp32c3 build_flags = - ${esp32_base.build_flags} + ${esp32c3_base.build_flags} -D HELTEC_HRU_3601 -I variants/esp32c3/heltec_hru_3601 lib_deps = ${esp32c3_base.lib_deps} - adafruit/Adafruit NeoPixel @ ^1.12.0 + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 diff --git a/variants/esp32c3/m5stack-stamp-c3/platformio.ini b/variants/esp32c3/m5stack-stamp-c3/platformio.ini index 1072df664..9ea669014 100644 --- a/variants/esp32c3/m5stack-stamp-c3/platformio.ini +++ b/variants/esp32c3/m5stack-stamp-c3/platformio.ini @@ -3,7 +3,7 @@ extends = esp32c3_base board = esp32-c3-devkitm-1 board_level = extra build_flags = - ${esp32_base.build_flags} + ${esp32c3_base.build_flags} -D PRIVATE_HW -I variants/esp32c3/m5stack-stamp-c3 monitor_speed = 115200 diff --git a/variants/esp32c6/esp32c6.ini b/variants/esp32c6/esp32c6.ini index b07a2dcd4..9ee8591be 100644 --- a/variants/esp32c6/esp32c6.ini +++ b/variants/esp32c6/esp32c6.ini @@ -1,5 +1,5 @@ [esp32c6_base] -extends = esp32_base +extends = esp32_common platform = # Do not renovate until we have switched to pioarduino tagged builds https://github.com/Jason2866/platform-espressif32/archive/22faa566df8c789000f8136cd8d0aca49617af55.zip @@ -28,19 +28,20 @@ lib_deps = ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib - lewisxhe/XPowersLib@0.3.2 + lewisxhe/XPowersLib@0.3.3 # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 build_src_filter = - ${esp32_base.build_src_filter} - + ${esp32_common.build_src_filter} - monitor_speed = 460800 monitor_filters = esp32_c3_exception_decoder lib_ignore = + ${esp32_common.lib_ignore} NonBlockingRTTTL NimBLE-Arduino libpax diff --git a/variants/esp32c6/m5stack_unitc6l/platformio.ini b/variants/esp32c6/m5stack_unitc6l/platformio.ini index 9992ab2bf..ed26598d2 100644 --- a/variants/esp32c6/m5stack_unitc6l/platformio.ini +++ b/variants/esp32c6/m5stack_unitc6l/platformio.ini @@ -1,6 +1,17 @@ [env:m5stack-unitc6l] +custom_meshtastic_hw_model = 111 +custom_meshtastic_hw_model_slug = M5STACK_C6L +custom_meshtastic_architecture = esp32-c6 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = M5Stack Unit C6L +custom_meshtastic_images = m5_c6l.svg +custom_meshtastic_tags = M5Stack + extends = esp32c6_base board = esp32-c6-devkitc-1 +board_upload.flash_size = 16MB +board_build.partitions = default_16MB.csv ;OpenOCD flash method ;upload_protocol = esp-builtin ;Normal method @@ -12,8 +23,10 @@ build_unflags = -D HAS_WIFI lib_deps = ${esp32c6_base.lib_deps} - adafruit/Adafruit NeoPixel@^1.12.3 - h2zero/NimBLE-Arduino@^2.3.6 + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 + # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino + h2zero/NimBLE-Arduino@2.3.7 build_flags = ${esp32c6_base.build_flags} -D M5STACK_UNITC6L diff --git a/variants/esp32c6/m5stack_unitc6l/variant.h b/variants/esp32c6/m5stack_unitc6l/variant.h index d973aa281..1654ee590 100644 --- a/variants/esp32c6/m5stack_unitc6l/variant.h +++ b/variants/esp32c6/m5stack_unitc6l/variant.h @@ -50,3 +50,5 @@ void c6l_init(); #endif #define SCREEN_TRANSITION_FRAMERATE 10 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness + +#define SERIAL_PRINT_PORT 1 diff --git a/variants/esp32c6/tlora_c6/platformio.ini b/variants/esp32c6/tlora_c6/platformio.ini index 6b402d7c5..174e5e297 100644 --- a/variants/esp32c6/tlora_c6/platformio.ini +++ b/variants/esp32c6/tlora_c6/platformio.ini @@ -8,3 +8,4 @@ build_flags = -I variants/esp32c6/tlora_c6 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 + -ULED_BUILTIN diff --git a/variants/esp32c6/tlora_c6/variant.h b/variants/esp32c6/tlora_c6/variant.h index 55635fe13..4a0d40232 100644 --- a/variants/esp32c6/tlora_c6/variant.h +++ b/variants/esp32c6/tlora_c6/variant.h @@ -19,3 +19,5 @@ #define SX126X_TXEN 14 #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define SERIAL_PRINT_PORT 1 diff --git a/variants/esp32s2/esp32s2.ini b/variants/esp32s2/esp32s2.ini index 0f97408b8..836e31d8d 100644 --- a/variants/esp32s2/esp32s2.ini +++ b/variants/esp32s2/esp32s2.ini @@ -1,19 +1,24 @@ [esp32s2_base] -extends = esp32_base +extends = esp32_common custom_esp32_kind = esp32s2 build_src_filter = - ${esp32_base.build_src_filter} - - - + ${esp32_common.build_src_filter} - - - monitor_speed = 115200 build_flags = - ${esp32_base.build_flags} + ${esp32_common.build_flags} -DHAS_BLUETOOTH=0 -DMESHTASTIC_EXCLUDE_PAXCOUNTER -DMESHTASTIC_EXCLUDE_BLUETOOTH - + +lib_deps = + ${esp32_common.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master + https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip + lib_ignore = - ${esp32_base.lib_ignore} + ${esp32_common.lib_ignore} NimBLE-Arduino libpax diff --git a/variants/esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h b/variants/esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h new file mode 100644 index 000000000..46415d30f --- /dev/null +++ b/variants/esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h @@ -0,0 +1,28 @@ +// Need this file for ESP32-S3 +// No need to modify this file, changes to pins imported from variant.h +// Most is similar to https://github.com/espressif/arduino-esp32/blob/master/variants/esp32s3/pins_arduino.h + +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// Serial +static const uint8_t TX = UART_TX; +static const uint8_t RX = UART_RX; + +// Default SPI will be mapped to Radio +static const uint8_t SS = LORA_CS; +static const uint8_t SCK = LORA_SCK; +static const uint8_t MOSI = LORA_MOSI; +static const uint8_t MISO = LORA_MISO; + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SCL = I2C_SCL; +static const uint8_t SDA = I2C_SDA; + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/CDEBYTE_EoRa-Hub/platformio.ini b/variants/esp32s3/CDEBYTE_EoRa-Hub/platformio.ini new file mode 100644 index 000000000..42c311a69 --- /dev/null +++ b/variants/esp32s3/CDEBYTE_EoRa-Hub/platformio.ini @@ -0,0 +1,8 @@ +[env:CDEBYTE_EoRa-Hub] +extends = esp32s3_base +board = CDEBYTE_EoRa-Hub +board_level = extra +build_flags = + ${esp32s3_base.build_flags} + -D PRIVATE_HW + -I variants/esp32s3/CDEBYTE_EoRa-Hub diff --git a/variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h b/variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h new file mode 100644 index 000000000..9bb3af45a --- /dev/null +++ b/variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h @@ -0,0 +1,20 @@ +#include "RadioLib.h" + +// This is rewritten to match the requirements of the E80-900M2213S +// The E80 does not conform to the reference Semtech switches(!) and therefore needs a custom matrix. +// See footnote #3 in "https://www.cdebyte.com/products/E80-900M2213S/2#Pin" +// RF Switch Matrix SubG RFO_HP_LF / RFO_LP_LF / RFI_[NP]_LF0 +// DIO5 -> RFSW0_V1 +// DIO6 -> RFSW1_V2 +// DIO7 -> not connected on E80 module - note that GNSS and Wifi scanning are not possible. + +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC, + RADIOLIB_NC}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 DIO7 + {LR11x0::MODE_STBY, {LOW, LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW, HIGH}}, + {LR11x0::MODE_WIFI, {LOW, LOW, LOW}}, END_OF_MODE_TABLE, +}; \ No newline at end of file diff --git a/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h b/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h new file mode 100644 index 000000000..1591f6395 --- /dev/null +++ b/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h @@ -0,0 +1,50 @@ +// EByte EoRA-Hub +// Uses E80 (LR1121) LoRa module + +#define LED_PIN 35 + +// Button - user interface +#define BUTTON_PIN 0 // BOOT button + +#define BATTERY_PIN 1 +#define ADC_CHANNEL ADC1_GPIO1_CHANNEL +#define ADC_MULTIPLIER 103.0 // Calibrated value +#define ADC_ATTENUATION ADC_ATTEN_DB_0 +#define ADC_CTRL 37 +#define ADC_CTRL_ENABLED LOW + +// Display - OLED connected via I2C by the default hardware configuration +#define HAS_SCREEN 1 +#define USE_SSD1306 +#define I2C_SCL 17 +#define I2C_SDA 18 + +// UART - The 1mm JST SH connector closest to the USB-C port +#define UART_TX 43 +#define UART_RX 44 + +// Peripheral I2C - The 1mm JST SH connector furthest from the USB-C port which follows Adafruit connection standard. There are no +// pull-up resistors on these lines, the downstream device needs to include them. TODO: test, currently untested +#define I2C_SCL1 21 +#define I2C_SDA1 10 + +// Radio +#define USE_LR1121 + +#define LORA_SCK 9 +#define LORA_MOSI 10 +#define LORA_MISO 11 +#define LORA_RESET 12 +#define LORA_CS 8 +#define LORA_DIO9 13 + +// LR1121 +#define LR1121_IRQ_PIN 14 +#define LR1121_NRESET_PIN LORA_RESET +#define LR1121_BUSY_PIN LORA_DIO9 +#define LR1121_SPI_NSS_PIN LORA_CS +#define LR1121_SPI_SCK_PIN LORA_SCK +#define LR1121_SPI_MOSI_PIN LORA_MOSI +#define LR1121_SPI_MISO_PIN LORA_MISO +#define LR11X0_DIO3_TCXO_VOLTAGE 1.8 +#define LR11X0_DIO_AS_RF_SWITCH diff --git a/variants/esp32s3/CDEBYTE_EoRa-S3/platformio.ini b/variants/esp32s3/CDEBYTE_EoRa-S3/platformio.ini index 3fcfbf281..092b36a2f 100644 --- a/variants/esp32s3/CDEBYTE_EoRa-S3/platformio.ini +++ b/variants/esp32s3/CDEBYTE_EoRa-S3/platformio.ini @@ -1,4 +1,13 @@ [env:CDEBYTE_EoRa-S3] +custom_meshtastic_hw_model = 61 +custom_meshtastic_hw_model_slug = CDEBYTE_EORA_S3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = EBYTE EoRa-S3 +custom_meshtastic_tags = EByte +custom_meshtastic_requires_dfu = true + extends = esp32s3_base board = CDEBYTE_EoRa-S3 build_flags = diff --git a/variants/esp32s3/ELECROW-ThinkNode-M2/platformio.ini b/variants/esp32s3/ELECROW-ThinkNode-M2/platformio.ini index 01e82184b..cfea4c1c0 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M2/platformio.ini +++ b/variants/esp32s3/ELECROW-ThinkNode-M2/platformio.ini @@ -1,4 +1,14 @@ [env:thinknode_m2] +custom_meshtastic_hw_model = 90 +custom_meshtastic_hw_model_slug = THINKNODE_M2 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = ThinkNode M2 +custom_meshtastic_images = thinknode_m2.svg +custom_meshtastic_tags = Elecrow +custom_meshtastic_requires_dfu = false + extends = esp32s3_base board = ESP32-S3-WROOM-1-N4 build_flags = diff --git a/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h index cd8d43555..ff4f883fe 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h +++ b/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h @@ -56,10 +56,6 @@ #define HAS_SCREEN 1 #define USE_SH1106 1 -// PCF8563 RTC Module -// #define PCF8563_RTC 0x51 -// #define PIN_RTC_INT 48 // Interrupt from the PCF8563 RTC -#define HAS_RTC 0 #define HAS_GPS 0 #define BUTTON_PIN PIN_BUTTON1 diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini index 7dac6e66e..ee51018d4 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini @@ -1,6 +1,20 @@ [env:thinknode_m5] +custom_meshtastic_hw_model = 107 +custom_meshtastic_hw_model_slug = THINKNODE_M5 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = ThinkNode M5 +custom_meshtastic_images = thinknode_m1.svg +custom_meshtastic_tags = Elecrow +custom_meshtastic_requires_dfu = false + extends = esp32s3_base board = ESP32-S3-WROOM-1-N4 +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/ELECROW-ThinkNode-M5> + build_flags = ${esp32s3_base.build_flags} -D ELECROW_ThinkNode_M5 @@ -16,6 +30,7 @@ build_flags = -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. lib_deps = ${esp32s3_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip - lewisxhe/PCF8563_Library@^1.0.1 - maxpromer/PCA9557-arduino @ ^1.0.0 \ No newline at end of file + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + # renovate: datasource=custom.pio depName=PCA9557-arduino packageName=maxpromer/library/PCA9557-arduino + maxpromer/PCA9557-arduino@1.0.0 diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp new file mode 100644 index 000000000..4b485a1a3 --- /dev/null +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp @@ -0,0 +1,12 @@ +#include "variant.h" +#include + +PCA9557 io(0x18, &Wire); + +void earlyInitVariant() +{ + Wire.begin(48, 47); + io.pinMode(PCA_PIN_EINK_EN, OUTPUT); + io.pinMode(PCA_PIN_POWER_EN, OUTPUT); + io.digitalWrite(PCA_PIN_POWER_EN, HIGH); +} diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h index 129b398e9..353741d91 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h @@ -4,6 +4,8 @@ #define UART_TX 43 #define UART_RX 44 +#define HAS_PCA9557 + // LED // Both of these are on the GPIO expander #define PCA_LED_USER 1 // the Blue LED @@ -44,9 +46,6 @@ #define PIN_SERIAL1_RX GPS_TX_PIN #define PIN_SERIAL1_TX GPS_RX_PIN -// PCF8563 RTC Module -#define PCF8563_RTC 0x51 - #define SX126X_CS 17 #define LORA_SCK 16 #define LORA_MOSI 15 @@ -82,4 +81,6 @@ #define BUTTON_PIN PIN_BUTTON1 #define BUTTON_PIN_ALT PIN_BUTTON2 + +#define SERIAL_PRINT_PORT 0 #endif diff --git a/variants/esp32s3/bpi_picow_esp32_s3/platformio.ini b/variants/esp32s3/bpi_picow_esp32_s3/platformio.ini index 57af0da82..e8c50adcc 100644 --- a/variants/esp32s3/bpi_picow_esp32_s3/platformio.ini +++ b/variants/esp32s3/bpi_picow_esp32_s3/platformio.ini @@ -8,9 +8,10 @@ board_level = extra upload_protocol = esptool ;upload_port = /dev/ttyACM2 lib_deps = - ${esp32_base.lib_deps} - caveman99/ESP32 Codec2@^1.0.1 + ${esp32s3_base.lib_deps} + # renovate: datasource=custom.pio depName=caveman99-ESP32_Codec2 packageName=caveman99/library/ESP32 Codec2 + caveman99/ESP32 Codec2@1.0.1 build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/esp32s3/bpi_picow_esp32_s3 diff --git a/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini b/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini index eed21a412..7a0bd31b4 100644 --- a/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini +++ b/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini @@ -25,7 +25,8 @@ build_flags = ;-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 lib_deps = ${esp32s3_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:crowpanel-esp32s3-4-epaper] extends = esp32s3_base @@ -54,7 +55,8 @@ build_flags = ;-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 lib_deps = ${esp32s3_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:crowpanel-esp32s3-2-epaper] extends = esp32s3_base @@ -83,4 +85,5 @@ build_flags = ;-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 lib_deps = ${esp32s3_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip diff --git a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini index 267544c40..f44f26006 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini +++ b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini @@ -9,14 +9,16 @@ upload_protocol = esptool ;upload_port = /dev/ttyACM1 upload_speed = 921600 lib_deps = - ${esp32_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 - adafruit/Adafruit NeoPixel @ ^1.12.0 + ${esp32s3_base.lib_deps} + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.6 + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/esp32s3/diy/my_esp32s3_diy_eink -Dmy diff --git a/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini b/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini index aa3e6e482..4f759d1e6 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini +++ b/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini @@ -9,13 +9,14 @@ upload_protocol = esptool ;upload_port = /dev/ttyACM0 upload_speed = 921600 lib_deps = - ${esp32_base.lib_deps} - adafruit/Adafruit NeoPixel @ ^1.12.0 + ${esp32s3_base.lib_deps} + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/esp32s3/diy/my_esp32s3_diy_oled -DBOARD_HAS_PSRAM diff --git a/variants/esp32s3/dreamcatcher/platformio.ini b/variants/esp32s3/dreamcatcher/platformio.ini index d088f2dac..c830346e0 100644 --- a/variants/esp32s3/dreamcatcher/platformio.ini +++ b/variants/esp32s3/dreamcatcher/platformio.ini @@ -12,8 +12,10 @@ build_flags = -D ARDUINO_USB_CDC_ON_BOOT=1 lib_deps = ${esp32s3_base.lib_deps} - earlephilhower/ESP8266Audio@^1.9.9 - earlephilhower/ESP8266SAM@^1.0.1 + # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio + earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM + earlephilhower/ESP8266SAM@1.1.0 [env:dreamcatcher-2206] extends = esp32s3_base diff --git a/variants/esp32s3/elecrow_panel/platformio.ini b/variants/esp32s3/elecrow_panel/platformio.ini index 065f22538..e0f6f0760 100644 --- a/variants/esp32s3/elecrow_panel/platformio.ini +++ b/variants/esp32s3/elecrow_panel/platformio.ini @@ -41,10 +41,13 @@ build_flags = ${esp32s3_base.build_flags} -Os lib_deps = ${esp32s3_base.lib_deps} ${device-ui_base.lib_deps} + # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio earlephilhower/ESP8266Audio@1.9.9 - earlephilhower/ESP8266SAM@1.0.1 - lovyan03/LovyanGFX@1.2.0 ; note: v1.2.7 breaks the elecrow 7" display functionality + # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM + earlephilhower/ESP8266SAM@1.1.0 + # renovate: datasource=custom.pio depName=TCA9534 packageName=hideakitai/library/TCA9534 hideakitai/TCA9534@0.1.1 + lovyan03/LovyanGFX@1.2.0 ; note: v1.2.7 breaks the elecrow 7" display functionality [crowpanel_small_esp32s3_base] ; 2.4, 2.8, 3.5 inch extends = crowpanel_base @@ -71,6 +74,17 @@ build_flags = -D DISPLAY_SET_RESOLUTION [env:elecrow-adv-24-28-tft] +custom_meshtastic_hw_model = 97 +custom_meshtastic_hw_model_slug = CROWPANEL +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Crowpanel Adv 2.4/2.8 TFT +custom_meshtastic_images = crowpanel_2_4.svg, crowpanel_2_8.svg +custom_meshtastic_tags = Elecrow +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = crowpanel_small_esp32s3_base build_flags = ${crowpanel_small_esp32s3_base.build_flags} @@ -95,6 +109,17 @@ build_flags = -D LGFX_TOUCH_ROTATION=0 [env:elecrow-adv-35-tft] +custom_meshtastic_hw_model = 97 +custom_meshtastic_hw_model_slug = CROWPANEL +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Crowpanel Adv 3.5 TFT +custom_meshtastic_images = crowpanel_3_5.svg +custom_meshtastic_tags = Elecrow +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = crowpanel_small_esp32s3_base board_level = pr build_flags = @@ -123,6 +148,17 @@ build_flags = ; 4.3, 5.0, 7.0 inch 800x480 IPS (V1) [env:elecrow-adv1-43-50-70-tft] +custom_meshtastic_hw_model = 97 +custom_meshtastic_hw_model_slug = CROWPANEL +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Crowpanel Adv 4.3/5.0/7.0 TFT +custom_meshtastic_images = crowpanel_5_0.svg, crowpanel_7_0.svg +custom_meshtastic_tags = Elecrow +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = crowpanel_large_esp32s3_base build_flags = ${crowpanel_large_esp32s3_base.build_flags} diff --git a/variants/esp32s3/esp32-s3-pico/platformio.ini b/variants/esp32s3/esp32-s3-pico/platformio.ini index 11bd4f5a3..db0c038e6 100644 --- a/variants/esp32s3/esp32-s3-pico/platformio.ini +++ b/variants/esp32s3/esp32-s3-pico/platformio.ini @@ -22,5 +22,7 @@ build_flags = ${esp32s3_base.build_flags} -DEINK_HEIGHT=128 lib_deps = ${esp32s3_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 - adafruit/Adafruit NeoPixel @ ^1.12.0 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.6 + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 diff --git a/variants/esp32s3/esp32-s3-pico/variant.h b/variants/esp32s3/esp32-s3-pico/variant.h index bfcb6059d..65732171a 100644 --- a/variants/esp32s3/esp32-s3-pico/variant.h +++ b/variants/esp32s3/esp32-s3-pico/variant.h @@ -8,7 +8,6 @@ #define EXT_NOTIFY_OUT 22 #define BUTTON_PIN 0 // 17 -// #define LED_PIN PIN_LED // Board has RGB LED 21 #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 1 // How many neopixels are connected diff --git a/variants/esp32s3/esp32s3.ini b/variants/esp32s3/esp32s3.ini index 8d8b6899e..299415442 100644 --- a/variants/esp32s3/esp32s3.ini +++ b/variants/esp32s3/esp32s3.ini @@ -1,5 +1,10 @@ [esp32s3_base] -extends = esp32_base +extends = esp32_common custom_esp32_kind = esp32s3 monitor_speed = 115200 + +lib_deps = + ${esp32_common.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master + https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip diff --git a/variants/esp32s3/hackaday-communicator/platformio.ini b/variants/esp32s3/hackaday-communicator/platformio.ini index 970215045..8fd275c0e 100644 --- a/variants/esp32s3/hackaday-communicator/platformio.ini +++ b/variants/esp32s3/hackaday-communicator/platformio.ini @@ -6,10 +6,15 @@ board_check = true board_build.partitions = default_16MB.csv upload_protocol = esptool +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/hackaday-communicator> + build_flags = ${esp32s3_base.build_flags} -D HACKADAY_COMMUNICATOR -D BOARD_HAS_PSRAM -I variants/esp32s3/hackaday-communicator lib_deps = ${esp32s3_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-Arduino_GFX packageName=https://github.com/meshtastic/Arduino_GFX gitBranch=master https://github.com/meshtastic/Arduino_GFX/archive/054e81ffaf23784830a734e3c184346789349406.zip \ No newline at end of file diff --git a/variants/esp32s3/hackaday-communicator/variant.cpp b/variants/esp32s3/hackaday-communicator/variant.cpp new file mode 100644 index 000000000..d85b2abb5 --- /dev/null +++ b/variants/esp32s3/hackaday-communicator/variant.cpp @@ -0,0 +1,6 @@ +#include "variant.h" +#include "Arduino.h" +void earlyInitVariant() +{ + pinMode(KB_INT, INPUT); +} \ No newline at end of file diff --git a/variants/esp32s3/hackaday-communicator/variant.h b/variants/esp32s3/hackaday-communicator/variant.h index a127f548f..cca408622 100644 --- a/variants/esp32s3/hackaday-communicator/variant.h +++ b/variants/esp32s3/hackaday-communicator/variant.h @@ -16,22 +16,12 @@ #define SLEEP_TIME 120 #define GPS_DEFAULT_NOT_PRESENT 1 -// #define GPS_RX_PIN 44 -// #define GPS_TX_PIN 43 - -// #define BATTERY_PIN 4 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage -// ratio of voltage divider = 2.0 (RD2=100k, RD3=100k) -// #define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage. -// #define ADC_CHANNEL ADC1_GPIO4_CHANNEL // keyboard #define I2C_SDA 47 // I2C pins for this board #define I2C_SCL 14 -// #define KB_POWERON -1 // must be set to HIGH -// #define KB_SLAVE_ADDRESS TDECK_KB_ADDR // 0x55 // #define KB_BL_PIN 46 // not used for now #define KB_INT 13 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define TFT_DC 39 #define TFT_CS 41 @@ -44,11 +34,9 @@ #define LORA_MOSI 3 #define LORA_CS 17 -// #define LORA_DIO0 -1 // a No connect on the SX1262 module #define LORA_RESET 18 #define LORA_DIO1 16 // SX1262 IRQ #define LORA_DIO2 15 // SX1262 BUSY -// #define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled #define SX126X_CS LORA_CS #define SX126X_DIO1 LORA_DIO1 @@ -58,4 +46,5 @@ #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -// #define LED_PIN 1 \ No newline at end of file +#define LED_NOTIFICATION 1 +#define LED_STATE_ON 0 diff --git a/variants/esp32s3/heltec_capsule_sensor_v3/platformio.ini b/variants/esp32s3/heltec_capsule_sensor_v3/platformio.ini index 0bb21581a..6dd828433 100644 --- a/variants/esp32s3/heltec_capsule_sensor_v3/platformio.ini +++ b/variants/esp32s3/heltec_capsule_sensor_v3/platformio.ini @@ -6,4 +6,5 @@ board_build.partitions = default_8MB.csv build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/heltec_capsule_sensor_v3 -D HELTEC_CAPSULE_SENSOR_V3 + -ULED_BUILTIN ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output diff --git a/variants/esp32s3/heltec_sensor_hub/platformio.ini b/variants/esp32s3/heltec_sensor_hub/platformio.ini index 92b90d9b9..3f93c7ad2 100644 --- a/variants/esp32s3/heltec_sensor_hub/platformio.ini +++ b/variants/esp32s3/heltec_sensor_hub/platformio.ini @@ -7,6 +7,8 @@ build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/heltec_sensor_hub -D HELTEC_SENSOR_HUB + -ULED_BUILTIN lib_deps = ${esp32s3_base.lib_deps} - adafruit/Adafruit NeoPixel @ ^1.12.0 + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 diff --git a/variants/esp32s3/heltec_v3/platformio.ini b/variants/esp32s3/heltec_v3/platformio.ini index af0854e49..fe31df094 100644 --- a/variants/esp32s3/heltec_v3/platformio.ini +++ b/variants/esp32s3/heltec_v3/platformio.ini @@ -1,4 +1,14 @@ -[env:heltec-v3] +[env:heltec-v3] +custom_meshtastic_hw_model = 43 +custom_meshtastic_hw_model_slug = HELTEC_V3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec V3 +custom_meshtastic_images = heltec-v3.svg, heltec-v3-case.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_wifi_lora_32_V3 board_level = pr @@ -8,3 +18,4 @@ build_flags = ${esp32s3_base.build_flags} -D HELTEC_V3 -I variants/esp32s3/heltec_v3 + -ULED_BUILTIN diff --git a/variants/esp32s3/heltec_v4/pins_arduino.h b/variants/esp32s3/heltec_v4/pins_arduino.h index d4485016d..32fd8a8e4 100644 --- a/variants/esp32s3/heltec_v4/pins_arduino.h +++ b/variants/esp32s3/heltec_v4/pins_arduino.h @@ -6,10 +6,6 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -static const uint8_t LED_BUILTIN = 35; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN // allow testing #ifdef LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini index 4ff7ff253..20b3b4606 100644 --- a/variants/esp32s3/heltec_v4/platformio.ini +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -7,11 +7,20 @@ build_flags = ${esp32s3_base.build_flags} -D HELTEC_V4 -I variants/esp32s3/heltec_v4 -lib_deps = - ${esp32s3_base.lib_deps} [env:heltec-v4] +custom_meshtastic_hw_model = 110 +custom_meshtastic_hw_model_slug = HELTEC_V4 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec V4 +custom_meshtastic_images = heltec_v4.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = heltec_v4_base build_flags = ${heltec_v4_base.build_flags} @@ -23,10 +32,19 @@ build_flags = -D I2C_SCL=18 -D I2C_SDA1=4 -D I2C_SCL1=3 -lib_deps = - ${heltec_v4_base.lib_deps} [env:heltec-v4-tft] +custom_meshtastic_hw_model = 110 +custom_meshtastic_hw_model_slug = HELTEC_V4 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec V4 TFT +custom_meshtastic_images = heltec_v4.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = heltec_v4_base build_flags = ${heltec_v4_base.build_flags} ;-Os @@ -107,6 +125,10 @@ build_flags = lib_deps = ${heltec_v4_base.lib_deps} ; ${device-ui_base.lib_deps} - lovyan03/LovyanGFX@1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 + # renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip + ; TODO revert to official device-ui (when merged) + # renovate: datasource=git-refs depName=Quency-D_device-ui packageName=https://github.com/Quency-D/device-ui gitBranch=heltec-v4-tft https://github.com/Quency-D/device-ui/archive/7c9870b8016641190b059bdd90fe16c1012a39eb.zip diff --git a/variants/esp32s3/heltec_v4/variant.h b/variants/esp32s3/heltec_v4/variant.h index 6524bbc72..1c1168d94 100644 --- a/variants/esp32s3/heltec_v4/variant.h +++ b/variants/esp32s3/heltec_v4/variant.h @@ -29,10 +29,32 @@ #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -#define USE_GC1109_PA // We have a GC1109 power amplifier+attenuator -#define LORA_PA_POWER 7 // power en -#define LORA_PA_EN 2 -#define LORA_PA_TX_EN 46 // enable tx +// ---- GC1109 RF FRONT END CONFIGURATION ---- +// The Heltec V4 uses a GC1109 FEM chip with integrated PA and LNA +// RF path: SX1262 -> GC1109 PA -> Pi attenuator -> Antenna +// Measured net TX gain (non-linear due to PA compression): +// +11dB at 0-15dBm input (e.g., 10dBm in -> 21dBm out) +// +10dB at 16-17dBm input +// +9dB at 18-19dBm input +// +7dB at 21dBm input (e.g., 21dBm in -> 28dBm out max) +// Control logic (from GC1109 datasheet): +// Shutdown: CSD=0, CTX=X, CPS=X +// Receive LNA: CSD=1, CTX=0, CPS=X (17dB gain, 2dB NF) +// Transmit bypass: CSD=1, CTX=1, CPS=0 (~1dB loss, no PA) +// Transmit PA: CSD=1, CTX=1, CPS=1 (full PA enabled) +// Pin mapping: +// CTX (pin 6) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) +// CSD (pin 4) -> GPIO2: Chip enable (HIGH=on, LOW=shutdown) +// CPS (pin 5) -> GPIO46: PA mode select (HIGH=full PA, LOW=bypass) +// VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 +#define USE_GC1109_PA +#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 LDO power enable +#define LORA_PA_EN 2 // CSD - GC1109 chip enable (HIGH=on) +#define LORA_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) + +// GC1109 FEM: TX/RX path switching is handled by DIO2 -> CTX pin (via SX126X_DIO2_AS_RF_SWITCH) +// GPIO46 is CPS (PA mode), not TX control - setTransmitEnable() handles it in SX126xInterface.cpp +// Do NOT use SX126X_TXEN/RXEN as that would cause double-control of GPIO46 #if HAS_TFT #define USE_TFTDISPLAY 1 diff --git a/variants/esp32s3/heltec_vision_master_e213/pins_arduino.h b/variants/esp32s3/heltec_vision_master_e213/pins_arduino.h index 56f5ef157..5cf3c6453 100644 --- a/variants/esp32s3/heltec_vision_master_e213/pins_arduino.h +++ b/variants/esp32s3/heltec_vision_master_e213/pins_arduino.h @@ -3,10 +3,6 @@ #include -static const uint8_t LED_BUILTIN = 45; // LED is not populated on earliest board variant -#define BUILTIN_LED LED_BUILTIN // Backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/heltec_vision_master_e213/platformio.ini b/variants/esp32s3/heltec_vision_master_e213/platformio.ini index 43f6199af..4ace5a45a 100644 --- a/variants/esp32s3/heltec_vision_master_e213/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_e213/platformio.ini @@ -1,4 +1,15 @@ [env:heltec-vision-master-e213] +custom_meshtastic_hw_model = 67 +custom_meshtastic_hw_model_slug = HELTEC_VISION_MASTER_E213 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Vision Master E213 +custom_meshtastic_images = heltec-vision-master-e213.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_vision_master_e213 board_build.partitions = default_8MB.csv @@ -17,8 +28,8 @@ build_flags = -DEINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" lib_deps = ${esp32s3_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip upload_speed = 115200 [env:heltec-vision-master-e213-inkhud] @@ -27,7 +38,7 @@ board = heltec_vision_master_e213 board_level = pr board_build.partitions = default_8MB.csv build_src_filter = - ${esp32_base.build_src_filter} + ${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = ${esp32s3_base.build_flags} diff --git a/variants/esp32s3/heltec_vision_master_e290/pins_arduino.h b/variants/esp32s3/heltec_vision_master_e290/pins_arduino.h index 56f5ef157..5cf3c6453 100644 --- a/variants/esp32s3/heltec_vision_master_e290/pins_arduino.h +++ b/variants/esp32s3/heltec_vision_master_e290/pins_arduino.h @@ -3,10 +3,6 @@ #include -static const uint8_t LED_BUILTIN = 45; // LED is not populated on earliest board variant -#define BUILTIN_LED LED_BUILTIN // Backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/heltec_vision_master_e290/platformio.ini b/variants/esp32s3/heltec_vision_master_e290/platformio.ini index 08056b639..e86746b67 100644 --- a/variants/esp32s3/heltec_vision_master_e290/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_e290/platformio.ini @@ -1,5 +1,16 @@ ; Using the original screen class [env:heltec-vision-master-e290] +custom_meshtastic_hw_model = 68 +custom_meshtastic_hw_model_slug = HELTEC_VISION_MASTER_E290 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Vision Master E290 +custom_meshtastic_images = heltec-vision-master-e290.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_vision_master_e290 board_build.partitions = default_8MB.csv @@ -20,8 +31,8 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/448c8538129fde3d02a7cb5e6fc81971ad92547f.zip - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip upload_speed = 115200 [env:heltec-vision-master-e290-inkhud] @@ -29,7 +40,7 @@ extends = esp32s3_base, inkhud board = heltec_vision_master_e290 board_build.partitions = default_8MB.csv build_src_filter = - ${esp32_base.build_src_filter} + ${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = ${esp32s3_base.build_flags} diff --git a/variants/esp32s3/heltec_vision_master_t190/pins_arduino.h b/variants/esp32s3/heltec_vision_master_t190/pins_arduino.h index eeef95ff1..b8a33f721 100644 --- a/variants/esp32s3/heltec_vision_master_t190/pins_arduino.h +++ b/variants/esp32s3/heltec_vision_master_t190/pins_arduino.h @@ -3,10 +3,6 @@ #include -static const uint8_t LED_BUILTIN = 35; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/heltec_vision_master_t190/platformio.ini b/variants/esp32s3/heltec_vision_master_t190/platformio.ini index e7e7ff4e4..bbc518b39 100644 --- a/variants/esp32s3/heltec_vision_master_t190/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_t190/platformio.ini @@ -1,4 +1,15 @@ [env:heltec-vision-master-t190] +custom_meshtastic_hw_model = 66 +custom_meshtastic_hw_model_slug = HELTEC_VISION_MASTER_T190 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Vision Master T190 +custom_meshtastic_images = heltec-vision-master-t190.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_vision_master_t190 board_build.partitions = default_8MB.csv @@ -8,6 +19,6 @@ build_flags = -D HELTEC_VISION_MASTER_T190 lib_deps = ${esp32s3_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main https://github.com/meshtastic/st7789/archive/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip upload_speed = 921600 diff --git a/variants/esp32s3/heltec_wireless_paper/pins_arduino.h b/variants/esp32s3/heltec_wireless_paper/pins_arduino.h index 3e36d98f5..886cab254 100644 --- a/variants/esp32s3/heltec_wireless_paper/pins_arduino.h +++ b/variants/esp32s3/heltec_wireless_paper/pins_arduino.h @@ -3,10 +3,6 @@ #include -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/heltec_wireless_paper/platformio.ini b/variants/esp32s3/heltec_wireless_paper/platformio.ini index f16dcd257..673c834ea 100644 --- a/variants/esp32s3/heltec_wireless_paper/platformio.ini +++ b/variants/esp32s3/heltec_wireless_paper/platformio.ini @@ -1,5 +1,15 @@ ; Using the original screen class [env:heltec-wireless-paper] +custom_meshtastic_hw_model = 49 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_PAPER +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Wireless Paper +custom_meshtastic_images = heltec-wireless-paper.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_wifi_lora_32_V3 board_build.partitions = default_8MB.csv @@ -18,8 +28,8 @@ build_flags = -D EINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" lib_deps = ${esp32s3_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip upload_speed = 115200 [env:heltec-wireless-paper-inkhud] @@ -27,7 +37,7 @@ extends = esp32s3_base, inkhud board = heltec_wifi_lora_32_V3 board_build.partitions = default_8MB.csv build_src_filter = - ${esp32_base.build_src_filter} + ${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = ${esp32s3_base.build_flags} diff --git a/variants/esp32s3/heltec_wireless_paper_v1/pins_arduino.h b/variants/esp32s3/heltec_wireless_paper_v1/pins_arduino.h index 2bb44161a..0c486eebc 100644 --- a/variants/esp32s3/heltec_wireless_paper_v1/pins_arduino.h +++ b/variants/esp32s3/heltec_wireless_paper_v1/pins_arduino.h @@ -3,10 +3,6 @@ #include -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t KEY_BUILTIN = 0; static const uint8_t TX = 43; diff --git a/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini b/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini index 99f2eddeb..8543e414f 100644 --- a/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini +++ b/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini @@ -1,4 +1,14 @@ [env:heltec-wireless-paper-v1_0] +custom_meshtastic_hw_model = 57 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_PAPER_V1_0 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = false +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Heltec Wireless Paper V1.0 +custom_meshtastic_images = heltec-wireless-paper-v1_0.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board_level = extra board = heltec_wifi_lora_32_V3 @@ -15,6 +25,6 @@ build_flags = -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated lib_deps = ${esp32s3_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip upload_speed = 115200 diff --git a/variants/esp32s3/heltec_wireless_tracker/pins_arduino.h b/variants/esp32s3/heltec_wireless_tracker/pins_arduino.h index 1052af961..93fd5d9c2 100644 --- a/variants/esp32s3/heltec_wireless_tracker/pins_arduino.h +++ b/variants/esp32s3/heltec_wireless_tracker/pins_arduino.h @@ -11,10 +11,6 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/heltec_wireless_tracker/platformio.ini b/variants/esp32s3/heltec_wireless_tracker/platformio.ini index 3a373bf4f..33643c541 100644 --- a/variants/esp32s3/heltec_wireless_tracker/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker/platformio.ini @@ -1,4 +1,15 @@ [env:heltec-wireless-tracker] +custom_meshtastic_hw_model = 48 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Wireless Tracker V1.1 +custom_meshtastic_images = heltec-wireless-tracker.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_wireless_tracker board_build.partitions = default_8MB.csv @@ -12,4 +23,5 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 diff --git a/variants/esp32s3/heltec_wireless_tracker_V1_0/pins_arduino.h b/variants/esp32s3/heltec_wireless_tracker_V1_0/pins_arduino.h index 28b982012..7a1f43eee 100644 --- a/variants/esp32s3/heltec_wireless_tracker_V1_0/pins_arduino.h +++ b/variants/esp32s3/heltec_wireless_tracker_V1_0/pins_arduino.h @@ -11,10 +11,6 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini b/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini index cd961533d..ab6592afb 100644 --- a/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini @@ -1,4 +1,14 @@ [env:heltec-wireless-tracker-V1-0] +custom_meshtastic_hw_model = 58 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER_V1_0 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = false +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Heltec Wireless Tracker V1.0 +custom_meshtastic_images = heltec-wireless-tracker.svg +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board_level = extra board = heltec_wireless_tracker @@ -11,4 +21,5 @@ build_flags = ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/pins_arduino.h b/variants/esp32s3/heltec_wireless_tracker_v2/pins_arduino.h index 61c319109..9fb825002 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/pins_arduino.h +++ b/variants/esp32s3/heltec_wireless_tracker_v2/pins_arduino.h @@ -10,10 +10,6 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini index 0f9265f91..a5277ba19 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini @@ -1,8 +1,17 @@ [env:heltec-wireless-tracker-v2] +custom_meshtastic_support_level = 1 +custom_meshtastic_images = heltec_wireless_tracker_v2.svg +custom_meshtastic_tags = Heltec + extends = esp32s3_base board = heltec_wireless_tracker_v2 board_build.partitions = default_8MB.csv upload_protocol = esptool +custom_meshtastic_hw_model = 113 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER_V2 +custom_meshtastic_architecture = esp32s3 +custom_meshtastic_display_name = Heltec Wireless Tracker V2 +custom_meshtastic_actively_supported = true build_flags = ${esp32s3_base.build_flags} @@ -10,4 +19,5 @@ build_flags = -D HELTEC_WIRELESS_TRACKER_V2 lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/variant.h b/variants/esp32s3/heltec_wireless_tracker_v2/variant.h index 0ce6b3e00..a5489173d 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/variant.h +++ b/variants/esp32s3/heltec_wireless_tracker_v2/variant.h @@ -73,7 +73,29 @@ #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -#define USE_GC1109_PA // We have a GC1109 power amplifier+attenuator -#define LORA_PA_POWER 7 // power en -#define LORA_PA_EN 4 -#define LORA_PA_TX_EN 46 // enable tx \ No newline at end of file +// ---- GC1109 RF FRONT END CONFIGURATION ---- +// The Heltec Wireless Tracker V2 uses a GC1109 FEM chip with integrated PA and LNA +// RF path: SX1262 -> GC1109 PA -> Pi attenuator -> Antenna +// Measured net TX gain (non-linear due to PA compression): +// +11dB at 0-15dBm input (e.g., 10dBm in -> 21dBm out) +// +10dB at 16-17dBm input +// +9dB at 18-19dBm input +// +7dB at 21dBm input (e.g., 21dBm in -> 28dBm out max) +// Control logic (from GC1109 datasheet): +// Shutdown: CSD=0, CTX=X, CPS=X +// Receive LNA: CSD=1, CTX=0, CPS=X (17dB gain, 2dB NF) +// Transmit bypass: CSD=1, CTX=1, CPS=0 (~1dB loss, no PA) +// Transmit PA: CSD=1, CTX=1, CPS=1 (full PA enabled) +// Pin mapping: +// CTX (pin 6) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) +// CSD (pin 4) -> GPIO4: Chip enable (HIGH=on, LOW=shutdown) +// CPS (pin 5) -> GPIO46: PA mode select (HIGH=full PA, LOW=bypass) +// VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 +#define USE_GC1109_PA +#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 LDO power enable +#define LORA_PA_EN 4 // CSD - GC1109 chip enable (HIGH=on) +#define LORA_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) + +// GC1109 FEM: TX/RX path switching is handled by DIO2 -> CTX pin (via SX126X_DIO2_AS_RF_SWITCH) +// GPIO46 is CPS (PA mode), not TX control - setTransmitEnable() handles it in SX126xInterface.cpp +// Do NOT use SX126X_TXEN/RXEN as that would cause double-control of GPIO46 \ No newline at end of file diff --git a/variants/esp32s3/heltec_wsl_v3/platformio.ini b/variants/esp32s3/heltec_wsl_v3/platformio.ini index c038a463e..873300c3c 100644 --- a/variants/esp32s3/heltec_wsl_v3/platformio.ini +++ b/variants/esp32s3/heltec_wsl_v3/platformio.ini @@ -1,4 +1,14 @@ -[env:heltec-wsl-v3] +[env:heltec-wsl-v3] +custom_meshtastic_hw_model = 44 +custom_meshtastic_hw_model_slug = HELTEC_WSL_V3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Wireless Stick Lite V3 +custom_meshtastic_images = heltec-wsl-v3.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_wifi_lora_32_V3 board_build.partitions = default_8MB.csv @@ -7,3 +17,4 @@ build_flags = ${esp32s3_base.build_flags} -D HELTEC_WSL_V3 -I variants/esp32s3/heltec_wsl_v3 + -ULED_BUILTIN diff --git a/variants/esp32s3/icarus/platformio.ini b/variants/esp32s3/icarus/platformio.ini index de450da93..0aaff52f5 100644 --- a/variants/esp32s3/icarus/platformio.ini +++ b/variants/esp32s3/icarus/platformio.ini @@ -7,9 +7,6 @@ board_build.mcu = esp32s3 board_build.partitions = default_8MB.csv upload_protocol = esptool upload_speed = 921600 -platform_packages = platformio/framework-arduinoespressif32@https://github.com/PowerFeather/powerfeather-meshtastic-arduino-lib/releases/download/2.0.16a/esp32-2.0.16.zip -lib_deps = - ${esp32s3_base.lib_deps} build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 diff --git a/variants/esp32s3/link32_s3_v1/platformio.ini b/variants/esp32s3/link32_s3_v1/platformio.ini index 8d88075c4..acce3dafb 100644 --- a/variants/esp32s3/link32_s3_v1/platformio.ini +++ b/variants/esp32s3/link32_s3_v1/platformio.ini @@ -1,8 +1,9 @@ [env:link32-s3-v1] extends = esp32s3_base board = esp32-s3-devkitc-1 +board_level = extra build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D LINK_32 -I variants/esp32s3/link32_s3_v1 -DARDUINO_USB_CDC_ON_BOOT diff --git a/variants/esp32s3/m5stack_cores3/pins_arduino.h b/variants/esp32s3/m5stack_cores3/pins_arduino.h index 78e936990..ff7d35993 100644 --- a/variants/esp32s3/m5stack_cores3/pins_arduino.h +++ b/variants/esp32s3/m5stack_cores3/pins_arduino.h @@ -10,10 +10,7 @@ // Some boards have too low voltage on this pin (board design bug) // Use different pin with 3V and connect with 48 // and change this setup for the chosen pin (for example 38) -static const uint8_t LED_BUILTIN = SOC_GPIO_PIN_COUNT + 48; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN -#define RGB_BUILTIN LED_BUILTIN +#define RGB_BUILTIN SOC_GPIO_PIN_COUNT + 48 #define RGB_BRIGHTNESS 64 static const uint8_t TX = 43; diff --git a/variants/esp32s3/m5stack_cores3/platformio.ini b/variants/esp32s3/m5stack_cores3/platformio.ini index 9973abfce..6ad09a8bf 100644 --- a/variants/esp32s3/m5stack_cores3/platformio.ini +++ b/variants/esp32s3/m5stack_cores3/platformio.ini @@ -6,8 +6,7 @@ board_check = true board_build.partitions = default_16MB.csv upload_protocol = esptool build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D PRIVATE_HW -D M5STACK_CORES3 -I variants/esp32s3/m5stack_cores3 -lib_deps = ${esp32_base.lib_deps} diff --git a/variants/esp32s3/mesh-tab/pins_arduino.h b/variants/esp32s3/mesh-tab/pins_arduino.h index c995f638c..d980e1a49 100644 --- a/variants/esp32s3/mesh-tab/pins_arduino.h +++ b/variants/esp32s3/mesh-tab/pins_arduino.h @@ -49,10 +49,6 @@ static const uint8_t T14 = 14; static const uint8_t VBAT_SENSE = 2; static const uint8_t VBUS_SENSE = 34; -// User LED -#define LED_BUILTIN 13 -#define BUILTIN_LED LED_BUILTIN // backward compatibility - static const uint8_t RGB_DATA = 40; // RGB_BUILTIN and RGB_BRIGHTNESS can be used in new Arduino API neopixelWrite() #define RGB_BUILTIN (RGB_DATA + SOC_GPIO_PIN_COUNT) diff --git a/variants/esp32s3/mesh-tab/platformio.ini b/variants/esp32s3/mesh-tab/platformio.ini index e21bc38e1..ecf5498b3 100644 --- a/variants/esp32s3/mesh-tab/platformio.ini +++ b/variants/esp32s3/mesh-tab/platformio.ini @@ -46,11 +46,12 @@ build_flags = ${esp32s3_base.build_flags} -D VIEW_320x240 -D USE_PACKET_API -I variants/esp32s3/mesh-tab -build_src_filter = ${esp32_base.build_src_filter} +build_src_filter = ${esp32s3_base.build_src_filter} lib_deps = - ${esp32_base.lib_deps} + ${esp32s3_base.lib_deps} ${device-ui_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 [mesh_tab_xpt2046] extends = mesh_tab_base diff --git a/variants/esp32s3/mesh-tab/variant.h b/variants/esp32s3/mesh-tab/variant.h index 63ef17d85..99204bba3 100644 --- a/variants/esp32s3/mesh-tab/variant.h +++ b/variants/esp32s3/mesh-tab/variant.h @@ -55,5 +55,6 @@ #define SX126X_RESET 14 #define SX126X_RXEN 47 #define SX126X_TXEN RADIOLIB_NC // Assuming that DIO2 is connected to TXEN pin +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif diff --git a/variants/esp32s3/nibble_esp32/platformio.ini b/variants/esp32s3/nibble_esp32/platformio.ini index 2f6960d2e..8e8cead16 100644 --- a/variants/esp32s3/nibble_esp32/platformio.ini +++ b/variants/esp32s3/nibble_esp32/platformio.ini @@ -3,6 +3,6 @@ extends = esp32s3_base board = esp32-s3-zero board_level = extra build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/esp32s3/nibble_esp32 diff --git a/variants/esp32s3/nugget_s3_lora/variant.h b/variants/esp32s3/nugget_s3_lora/variant.h index 8e6057d5b..1354d0837 100644 --- a/variants/esp32s3/nugget_s3_lora/variant.h +++ b/variants/esp32s3/nugget_s3_lora/variant.h @@ -12,7 +12,7 @@ #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use // 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_PIN 43 // If defined, this will be used for user button presses #define BUTTON_NEED_PULLUP #define USE_RF95 @@ -20,8 +20,19 @@ #define LORA_MISO 7 #define LORA_MOSI 8 #define LORA_CS 9 -#define LORA_DIO0 16 // a No connect on the SX1262 module +#define LORA_DIO0 16 #define LORA_RESET 4 #define LORA_DIO1 RADIOLIB_NC -#define LORA_DIO2 RADIOLIB_NC \ No newline at end of file +#define LORA_DIO2 RADIOLIB_NC + +// jk, its not really a trackball but we're gonna pretend! +#define HAS_TRACKBALL 1 +#define TB_UP 13 +#define TB_DOWN 18 +#define TB_LEFT 11 +#define TB_RIGHT 12 +#define TB_PRESS 44 // BUTTON_PIN +#define TB_DIRECTION FALLING + +#define ENABLE_AMBIENTLIGHTING diff --git a/variants/esp32s3/picomputer-s3/platformio.ini b/variants/esp32s3/picomputer-s3/platformio.ini index 4131cc30a..82147f222 100644 --- a/variants/esp32s3/picomputer-s3/platformio.ini +++ b/variants/esp32s3/picomputer-s3/platformio.ini @@ -1,4 +1,12 @@ [env:picomputer-s3] +custom_meshtastic_hw_model = 52 +custom_meshtastic_hw_model_slug = PICOMPUTER_S3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Pi Computer S3 +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = bpi_picow_esp32_s3 board_check = true @@ -15,7 +23,8 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 build_src_filter = ${esp32s3_base.build_src_filter} diff --git a/variants/esp32s3/picomputer-s3/variant.h b/variants/esp32s3/picomputer-s3/variant.h index 275da1b61..7b6218f87 100644 --- a/variants/esp32s3/picomputer-s3/variant.h +++ b/variants/esp32s3/picomputer-s3/variant.h @@ -52,8 +52,6 @@ // Picomputer gets a white on black display #define TFT_MESH_OVERRIDE COLOR565(255, 255, 255) -#define CANNED_MESSAGE_MODULE_ENABLE 1 - #define INPUTBROKER_MATRIX_TYPE 1 #define KEYS_COLS \ diff --git a/variants/esp32s3/rak3312/pins_arduino.h b/variants/esp32s3/rak3312/pins_arduino.h index 15a26e991..600c619cf 100644 --- a/variants/esp32s3/rak3312/pins_arduino.h +++ b/variants/esp32s3/rak3312/pins_arduino.h @@ -17,12 +17,58 @@ static const uint8_t MOSI = 11; static const uint8_t MISO = 10; static const uint8_t SCK = 13; +#define SPI_INTERFACES_COUNT 1 + #define SPI_MOSI (11) #define SPI_SCK (13) #define SPI_MISO (10) #define SPI_CS (12) -// LEDs -#define LED_BUILTIN LED_GREEN +#ifdef _VARIANT_RAK3112_ +/* + * Serial interfaces + */ +// TXD1 RXD1 on Base Board +#define PIN_SERIAL1_RX (44) +#define PIN_SERIAL1_TX (43) + +/* + * Internal SPI to LoRa transceiver + */ +#define LORA_SX126X_SCK 5 +#define LORA_SX126X_MISO 3 +#define LORA_SX126X_MOSI 6 + +/* + * Analog pins + */ +#define PIN_A0 (21) +#define PIN_A1 (14) + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 2 + +#define PIN_WIRE1_SDA (17) +#define PIN_WIRE1_SCL (18) + +/* + * GPIO's + */ +#define WB_IO1 21 +#define WB_IO2 2 +// #define WB_IO2 14 +#define WB_IO3 41 +#define WB_IO4 42 +#define WB_IO5 38 +#define WB_IO6 39 +// #define WB_SW1 35 NC +#define WB_A0 1 +#define WB_A1 2 +#define WB_CS 12 +#define WB_LED1 46 +#define WB_LED2 45 +#endif #endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/rak3312/platformio.ini b/variants/esp32s3/rak3312/platformio.ini index 0de36498f..113c2f527 100644 --- a/variants/esp32s3/rak3312/platformio.ini +++ b/variants/esp32s3/rak3312/platformio.ini @@ -1,4 +1,15 @@ [env:rak3312] +custom_meshtastic_hw_model = 106 +custom_meshtastic_hw_model_slug = RAK3312 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = RAK3312 +custom_meshtastic_images = rak_3312.svg +custom_meshtastic_tags = RAK +custom_meshtastic_requires_dfu = false +custom_meshtastic_partition_scheme = 16MB + extends = esp32s3_base board = wiscore_rak3312 board_level = pr @@ -6,6 +17,18 @@ board_check = true upload_protocol = esptool build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D RAK3312 -I variants/esp32s3/rak3312 + +[env:rak3112] +extends = esp32s3_base +board = wiscore_rak3312 +board_level = extra +upload_protocol = esptool + +build_flags = + ${esp32_base.build_flags} + -D RAK3312 + -D _VARIANT_RAK3112_ + -I variants/esp32s3/rak3312 diff --git a/variants/esp32s3/rak3312/variant.h b/variants/esp32s3/rak3312/variant.h index dfdf4de71..6431f1fd0 100644 --- a/variants/esp32s3/rak3312/variant.h +++ b/variants/esp32s3/rak3312/variant.h @@ -18,27 +18,41 @@ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif -#define SX126X_POWER_EN (4) - -#define PIN_POWER_EN PIN_3V3_EN -#define PIN_3V3_EN (14) - #define LED_GREEN 46 #define LED_BLUE 45 #define PIN_LED1 LED_GREEN -#define PIN_LED2 LED_BLUE +#define LED_NOTIFICATION LED_BLUE -#define LED_CONN LED_BLUE #define LED_PIN LED_GREEN #define ledOff(pin) pinMode(pin, INPUT) #define LED_STATE_ON 1 // State when LED is litted +#define BATTERY_PIN 1 +#define ADC_CHANNEL ADC1_GPIO1_CHANNEL + +#ifdef _VARIANT_RAK3112_ // Modular variant (stamp) +#define ADC_MULTIPLIER 2.11 + +#define BUTTON_NEED_PULLUP + +#define HAS_SDCARD +#define SDCARD_USE_SPI1 +#define SDCARD_CS SPI_CS + +#define I2C_SDA1 PIN_WIRE1_SDA +#define I2C_SCL1 PIN_WIRE1_SCL +#else // Generic 3312 variant (40-pin standard connector) +#define ADC_MULTIPLIER 1.667 + +#define SX126X_POWER_EN (4) + +#define PIN_POWER_EN PIN_3V3_EN +#define PIN_3V3_EN (14) + #define HAS_GPS 1 #define GPS_TX_PIN 43 #define GPS_RX_PIN 44 -#define BATTERY_PIN 1 -#define ADC_CHANNEL ADC1_GPIO1_CHANNEL -#define ADC_MULTIPLIER 1.667 \ No newline at end of file +#endif \ No newline at end of file diff --git a/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h b/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h index 15a26e991..b51cd214e 100644 --- a/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h +++ b/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h @@ -22,7 +22,4 @@ static const uint8_t SCK = 13; #define SPI_MISO (10) #define SPI_CS (12) -// LEDs -#define LED_BUILTIN LED_GREEN - #endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini index de4714efa..8423bb4df 100644 --- a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini +++ b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini @@ -8,14 +8,15 @@ upload_protocol = esptool board_build.partitions = default_8MB.csv build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D RAK3312 -D RAK_WISMESH_TAP_V2 -I variants/esp32s3/rak_wismesh_tap_v2 lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 [ft5x06] extends = mesh_tab_base diff --git a/variants/esp32s3/rak_wismesh_tap_v2/variant.h b/variants/esp32s3/rak_wismesh_tap_v2/variant.h index 2fc056557..7d263165c 100644 --- a/variants/esp32s3/rak_wismesh_tap_v2/variant.h +++ b/variants/esp32s3/rak_wismesh_tap_v2/variant.h @@ -30,9 +30,8 @@ #define LED_BLUE 45 #define PIN_LED1 LED_GREEN -#define PIN_LED2 LED_BLUE +#define LED_NOTIFICATION LED_BLUE -#define LED_CONN LED_BLUE #define LED_PIN LED_GREEN #define ledOff(pin) pinMode(pin, INPUT) @@ -47,10 +46,8 @@ #define SPI_MISO (10) #define SPI_CS (12) -#define HAS_BUTTON 1 #define BUTTON_PIN 0 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define USE_VIRTUAL_KEYBOARD 1 #define BATTERY_PIN 1 diff --git a/variants/esp32s3/seeed-sensecap-indicator/pins_arduino.h b/variants/esp32s3/seeed-sensecap-indicator/pins_arduino.h index 300f0e0f5..88c233491 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/pins_arduino.h +++ b/variants/esp32s3/seeed-sensecap-indicator/pins_arduino.h @@ -3,8 +3,6 @@ #include -// static const uint8_t LED_BUILTIN = -1; - // static const uint8_t TX = 43; // static const uint8_t RX = 44; diff --git a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini index 25ec3ebfc..70a10e0d4 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini +++ b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini @@ -1,5 +1,16 @@ ; Seeed Studio SenseCAP Indicator [env:seeed-sensecap-indicator] +custom_meshtastic_hw_model = 70 +custom_meshtastic_hw_model_slug = SENSECAP_INDICATOR +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Seeed SenseCAP Indicator +custom_meshtastic_images = seeed-sensecap-indicator.svg +custom_meshtastic_tags = Seeed +custom_meshtastic_partition_scheme = 8MB + = true + extends = esp32s3_base platform_packages = platformio/framework-arduinoespressif32 @ https://github.com/mverch67/arduino-esp32/archive/aef7fef6de3329ed6f75512d46d63bba12b09bb5.zip ; add_tca9535 (based on 2.0.16) @@ -9,7 +20,7 @@ board_check = true board_build.partitions = partition-table-8MB.csv upload_protocol = esptool -build_flags = ${esp32_base.build_flags} +build_flags = ${esp32s3_base.build_flags} -Ivariants/esp32s3/seeed-sensecap-indicator -DSENSECAP_INDICATOR -DCONFIG_ARDUHAL_LOG_COLORS @@ -24,10 +35,12 @@ build_flags = ${esp32_base.build_flags} -DUSE_ARDUINO_HAL_GPIO lib_deps = ${esp32s3_base.lib_deps} + ; TODO switch back to official LovyanGFX https://github.com/mverch67/LovyanGFX/archive/4c76238c1344162a234ae917b27651af146d6fb2.zip - earlephilhower/ESP8266Audio@^1.9.9 - earlephilhower/ESP8266SAM@^1.0.1 - + # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio + earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM + earlephilhower/ESP8266SAM@1.1.0 [env:seeed-sensecap-indicator-tft] extends = env:seeed-sensecap-indicator @@ -64,4 +77,5 @@ build_flags = lib_deps = ${env:seeed-sensecap-indicator.lib_deps} ${device-ui_base.lib_deps} + ; TODO switch back to official bb_captouch https://github.com/mverch67/bb_captouch/archive/8626412fe650d808a267791c0eae6e5860c85a5d.zip ; alternative touch library supporting FT6x36 diff --git a/variants/esp32s3/seeed_xiao_s3/platformio.ini b/variants/esp32s3/seeed_xiao_s3/platformio.ini index ffc6e9638..b0e66241b 100644 --- a/variants/esp32s3/seeed_xiao_s3/platformio.ini +++ b/variants/esp32s3/seeed_xiao_s3/platformio.ini @@ -1,4 +1,15 @@ [env:seeed-xiao-s3] +custom_meshtastic_hw_model = 81 +custom_meshtastic_hw_model_slug = SEEED_XIAO_S3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Seeed Xiao ESP32-S3 +custom_meshtastic_images = seeed-xiao-s3.svg +custom_meshtastic_tags = Seeed +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = seeed-xiao-s3 board_level = pr @@ -6,8 +17,6 @@ board_check = true board_build.partitions = default_8MB.csv upload_protocol = esptool upload_speed = 921600 -lib_deps = - ${esp32s3_base.lib_deps} build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 diff --git a/variants/esp32s3/station-g2/platformio.ini b/variants/esp32s3/station-g2/platformio.ini index 056d543d9..091b35f00 100755 --- a/variants/esp32s3/station-g2/platformio.ini +++ b/variants/esp32s3/station-g2/platformio.ini @@ -1,4 +1,15 @@ [env:station-g2] +custom_meshtastic_hw_model = 31 +custom_meshtastic_hw_model_slug = STATION_G2 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 2 +custom_meshtastic_display_name = Station G2 +custom_meshtastic_images = station-g2.svg +custom_meshtastic_tags = B&Q +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = esp32s3_base board = station-g2 board_level = pr @@ -8,8 +19,6 @@ board_build.mcu = esp32s3 upload_protocol = esptool ;upload_port = /dev/ttyACM0 upload_speed = 921600 -lib_deps = - ${esp32s3_base.lib_deps} build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 diff --git a/variants/esp32s3/t-beam-1w/pins_arduino.h b/variants/esp32s3/t-beam-1w/pins_arduino.h new file mode 100644 index 000000000..c4591878b --- /dev/null +++ b/variants/esp32s3/t-beam-1w/pins_arduino.h @@ -0,0 +1,25 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +// I2C for OLED and sensors +static const uint8_t SDA = 8; +static const uint8_t SCL = 9; + +// Default SPI mapped to Radio/SD +static const uint8_t SS = 15; // LoRa CS +static const uint8_t MOSI = 11; +static const uint8_t MISO = 12; +static const uint8_t SCK = 13; + +// SD Card CS +#define SDCARD_CS 10 + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/t-beam-1w/platformio.ini b/variants/esp32s3/t-beam-1w/platformio.ini new file mode 100644 index 000000000..9abf895db --- /dev/null +++ b/variants/esp32s3/t-beam-1w/platformio.ini @@ -0,0 +1,23 @@ +; LilyGo T-Beam-1W (1 Watt LoRa with external PA) +[env:t-beam-1w] +custom_meshtastic_hw_model = 122 +custom_meshtastic_hw_model_slug = TBEAM_1_WATT +custom_meshtastic_architecture = esp32s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Beam 1W +custom_meshtastic_images = tbeam-1w.svg +custom_meshtastic_tags = LilyGo + +extends = esp32s3_base +board = t-beam-1w +board_build.partitions = default_8MB.csv +board_check = true + +lib_deps = + ${esp32s3_base.lib_deps} + +build_flags = + ${esp32s3_base.build_flags} + -I variants/esp32s3/t-beam-1w + -D T_BEAM_1W diff --git a/variants/esp32s3/t-beam-1w/variant.h b/variants/esp32s3/t-beam-1w/variant.h new file mode 100644 index 000000000..dbe1620e2 --- /dev/null +++ b/variants/esp32s3/t-beam-1w/variant.h @@ -0,0 +1,97 @@ +// LilyGo T-Beam-1W variant.h +// Configuration based on LilyGO utilities.h and RF documentation + +// I2C for OLED display (SH1106 at 0x3C) +#define I2C_SDA 8 +#define I2C_SCL 9 + +// GPS - Quectel L76K +#define GPS_RX_PIN 5 +#define GPS_TX_PIN 6 +#define GPS_1PPS_PIN 7 +#define GPS_WAKEUP_PIN 16 // GPS_EN_PIN in LilyGO code +#define HAS_GPS 1 +#define GPS_BAUDRATE 9600 + +// Buttons +#define BUTTON_PIN 0 // BUTTON 1 +#define ALT_BUTTON_PIN 17 // BUTTON 2 + +// SPI (shared by LoRa and SD) +#define SPI_MOSI 11 +#define SPI_SCK 13 +#define SPI_MISO 12 +#define SPI_CS 10 + +// SD Card +#define HAS_SDCARD +#define SDCARD_USE_SPI1 +#define SDCARD_CS SPI_CS + +// LoRa Radio - SX1262 with 1W PA +#define USE_SX1262 + +#define LORA_SCK SPI_SCK +#define LORA_MISO SPI_MISO +#define LORA_MOSI SPI_MOSI +#define LORA_CS 15 +#define LORA_RESET 3 +#define LORA_DIO1 1 +#define LORA_BUSY 38 + +// CRITICAL: Radio power enable - MUST be HIGH before lora.begin()! +// GPIO 40 powers the SX1262 + PA module via LDO +#define SX126X_POWER_EN 40 + +// TX power offset for external PA (0 = no offset, full SX1262 power) +#define TX_GAIN_LORA 10 + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_BUSY +#define SX126X_RESET LORA_RESET + +// RF switching configuration for 1W PA module +// DIO2 controls PA (via SX126X_DIO2_AS_RF_SWITCH) +// CTRL PIN (GPIO 21) controls LNA - must be HIGH during RX +// Truth table: DIO2=1,CTRL=0 → TX (PA on, LNA off) +// DIO2=0,CTRL=1 → RX (PA off, LNA on) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_RXEN 21 // LNA enable - HIGH during RX + +// TCXO voltage - required for radio init +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define SX126X_MAX_POWER 22 +#endif + +// LED +#define LED_PIN 18 +#define LED_STATE_ON 1 // HIGH = ON + +// Battery ADC +#define BATTERY_PIN 4 +#define ADC_CHANNEL ADC1_GPIO4_CHANNEL +#define BATTERY_SENSE_SAMPLES 30 +#define ADC_MULTIPLIER 2.9333 + +// NTC temperature sensor +#define NTC_PIN 14 + +// Fan control +#define FAN_CTRL_PIN 41 +// Meshtastic standard fan control pin macro +#define RF95_FAN_EN FAN_CTRL_PIN + +// PA Ramp Time - T-Beam 1W requires >800us stabilization (default is 200us) +// Value 0x05 = RADIOLIB_SX126X_PA_RAMP_800U +#define SX126X_PA_RAMP_US 0x05 + +// Display - SH1106 OLED (128x64) +#define USE_SH1106 +#define OLED_WIDTH 128 +#define OLED_HEIGHT 64 + +// 32768 Hz crystal present +#define HAS_32768HZ 1 diff --git a/variants/esp32s3/t-deck-pro/platformio.ini b/variants/esp32s3/t-deck-pro/platformio.ini index 0b3c7843a..5ba82d045 100644 --- a/variants/esp32s3/t-deck-pro/platformio.ini +++ b/variants/esp32s3/t-deck-pro/platformio.ini @@ -1,11 +1,26 @@ [env:t-deck-pro] +custom_meshtastic_hw_model = 102 +custom_meshtastic_hw_model_slug = T_DECK_PRO +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Deck Pro +custom_meshtastic_images = tdeck_pro.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = esp32s3_base board = t-deck-pro board_check = true upload_protocol = esptool +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/t-deck-pro> + build_flags = - ${esp32_base.build_flags} -I variants/esp32s3/t-deck-pro + ${esp32s3_base.build_flags} -I variants/esp32s3/t-deck-pro -D T_DECK_PRO -D USE_EINK -D EINK_DISPLAY_MODEL=GxEPD2_310_GDEQ031T10 @@ -17,7 +32,11 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} - https://github.com/ZinggJM/GxEPD2/archive/refs/tags/1.6.4.zip + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.6 + # renovate: datasource=git-refs depName=CSE_Touch packageName=https://github.com/CIRCUITSTATE/CSE_Touch gitBranch=main https://github.com/CIRCUITSTATE/CSE_Touch/archive/b44f23b6f870b848f1fbe453c190879bc6cfaafa.zip + # renovate: datasource=github-tags depName=CSE_CST328 packageName=CIRCUITSTATE/CSE_CST328 https://github.com/CIRCUITSTATE/CSE_CST328/archive/refs/tags/v0.0.4.zip + # renovate: datasource=git-refs depName=BQ27220 packageName=https://github.com/mverch67/BQ27220 gitBranch=main https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip diff --git a/variants/esp32s3/t-deck-pro/variant.cpp b/variants/esp32s3/t-deck-pro/variant.cpp new file mode 100644 index 000000000..509726c52 --- /dev/null +++ b/variants/esp32s3/t-deck-pro/variant.cpp @@ -0,0 +1,14 @@ +#include "variant.h" +#include "Arduino.h" + +void earlyInitVariant() +{ + pinMode(LORA_EN, OUTPUT); + digitalWrite(LORA_EN, HIGH); + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(PIN_EINK_CS, OUTPUT); + digitalWrite(PIN_EINK_CS, HIGH); +} \ No newline at end of file diff --git a/variants/esp32s3/t-deck-pro/variant.h b/variants/esp32s3/t-deck-pro/variant.h index 35cb99435..d95f07f3a 100644 --- a/variants/esp32s3/t-deck-pro/variant.h +++ b/variants/esp32s3/t-deck-pro/variant.h @@ -43,7 +43,6 @@ // TCA8418 keyboard #define KB_BL_PIN 42 -#define CANNED_MESSAGE_MODULE_ENABLE 1 // microphone PCM5102A #define PCM5102A_SCK 47 @@ -100,3 +99,5 @@ #define MODEM_DTR 8 #define MODEM_RX 10 #define MODEM_TX 11 + +#define HAS_PHYSICAL_KEYBOARD 1 \ No newline at end of file diff --git a/variants/esp32s3/t-deck/pins_arduino.h b/variants/esp32s3/t-deck/pins_arduino.h index cb429d776..c358b988e 100644 --- a/variants/esp32s3/t-deck/pins_arduino.h +++ b/variants/esp32s3/t-deck/pins_arduino.h @@ -6,8 +6,6 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -// static const uint8_t LED_BUILTIN = -1; - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/t-deck/platformio.ini b/variants/esp32s3/t-deck/platformio.ini index 9ab0756d1..c216ec595 100644 --- a/variants/esp32s3/t-deck/platformio.ini +++ b/variants/esp32s3/t-deck/platformio.ini @@ -1,20 +1,38 @@ ; LilyGo T-Deck [env:t-deck] +custom_meshtastic_hw_model = 50 +custom_meshtastic_hw_model_slug = T_DECK +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Deck +custom_meshtastic_images = t-deck.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = esp32s3_base board = t-deck board_check = true board_build.partitions = default_16MB.csv upload_protocol = esptool +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/t-deck> + build_flags = ${esp32s3_base.build_flags} -D T_DECK -D BOARD_HAS_PSRAM -I variants/esp32s3/t-deck lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 - earlephilhower/ESP8266Audio@^1.9.9 - earlephilhower/ESP8266SAM@^1.0.1 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 + # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio + earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM + earlephilhower/ESP8266SAM@1.1.0 [env:t-deck-tft] extends = env:t-deck @@ -68,4 +86,5 @@ build_flags = lib_deps = ${env:t-deck.lib_deps} ${device-ui_base.lib_deps} + # renovate: datasource=github-tags depName=bb_captouch packageName=bitbank2/bb_captouch https://github.com/bitbank2/bb_captouch/archive/refs/tags/1.3.1.zip diff --git a/variants/esp32s3/t-deck/variant.cpp b/variants/esp32s3/t-deck/variant.cpp new file mode 100644 index 000000000..6b68f142c --- /dev/null +++ b/variants/esp32s3/t-deck/variant.cpp @@ -0,0 +1,23 @@ +#include "variant.h" +#include "Arduino.h" + +void earlyInitVariant() +{ + // GPIO10 manages all peripheral power supplies + // Turn on peripheral power immediately after MUC starts. + // If some boards are turned on late, ESP32 will reset due to low voltage. + // ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) , + // TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder) + pinMode(KB_POWERON, OUTPUT); + digitalWrite(KB_POWERON, HIGH); + // T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus + // We need to initialize all CS pins in advance otherwise there will be SPI communication issues + // e.g. when detecting the SD card + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(TFT_CS, OUTPUT); + digitalWrite(TFT_CS, HIGH); + delay(100); +} \ No newline at end of file diff --git a/variants/esp32s3/t-deck/variant.h b/variants/esp32s3/t-deck/variant.h index ece0cdeaf..5d885579a 100644 --- a/variants/esp32s3/t-deck/variant.h +++ b/variants/esp32s3/t-deck/variant.h @@ -23,6 +23,7 @@ #define SCREEN_TRANSITION_FRAMERATE 5 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness #define USE_TFTDISPLAY 1 +#define HAS_PHYSICAL_KEYBOARD 1 #define HAS_TOUCHSCREEN 1 #define SCREEN_TOUCH_INT 16 @@ -60,7 +61,6 @@ #define KB_POWERON 10 // must be set to HIGH #define KB_SLAVE_ADDRESS TDECK_KB_ADDR // 0x55 #define KB_BL_PIN 46 // not used for now -#define CANNED_MESSAGE_MODULE_ENABLE 1 // trackball #define HAS_TRACKBALL 1 @@ -70,6 +70,7 @@ #define TB_RIGHT 2 #define TB_PRESS 0 // BUTTON_PIN #define TB_DIRECTION FALLING +#define TB_THRESHOLD 3 // microphone #define ES7210_SCK 47 diff --git a/variants/esp32s3/t-eth-elite/platformio.ini b/variants/esp32s3/t-eth-elite/platformio.ini index 1a5823bc3..5ed67e977 100644 --- a/variants/esp32s3/t-eth-elite/platformio.ini +++ b/variants/esp32s3/t-eth-elite/platformio.ini @@ -15,4 +15,5 @@ lib_ignore = lib_deps = ${esp32s3_base.lib_deps} + # renovate: datasource=github-tags depName=ETHClass2 packageName=meshtastic/ETHClass2 https://github.com/meshtastic/ETHClass2/archive/v1.0.0.zip diff --git a/variants/esp32s3/t-watch-s3/pins_arduino.h b/variants/esp32s3/t-watch-s3/pins_arduino.h index 35f0e933e..f4585ace8 100644 --- a/variants/esp32s3/t-watch-s3/pins_arduino.h +++ b/variants/esp32s3/t-watch-s3/pins_arduino.h @@ -3,8 +3,6 @@ #include -// static const uint8_t LED_BUILTIN = -1; - // static const uint8_t TX = 43; // static const uint8_t RX = 44; diff --git a/variants/esp32s3/t-watch-s3/platformio.ini b/variants/esp32s3/t-watch-s3/platformio.ini index 59ff8891d..8c79ca097 100644 --- a/variants/esp32s3/t-watch-s3/platformio.ini +++ b/variants/esp32s3/t-watch-s3/platformio.ini @@ -1,21 +1,33 @@ ; LilyGo T-Watch S3 [env:t-watch-s3] +custom_meshtastic_hw_model = 51 +custom_meshtastic_hw_model_slug = T_WATCH_S3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = LILYGO T-Watch S3 +custom_meshtastic_images = t-watch-s3.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = t-watch-s3 board_check = true board_build.partitions = default_16MB.csv upload_protocol = esptool -build_flags = ${esp32_base.build_flags} +build_flags = ${esp32s3_base.build_flags} -DT_WATCH_S3 -Ivariants/esp32s3/t-watch-s3 - -DPCF8563_RTC=0x51 - -DHAS_BMA423=1 lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 - lewisxhe/PCF8563_Library@1.0.1 - adafruit/Adafruit DRV2605 Library@^1.2.2 - earlephilhower/ESP8266Audio@^1.9.9 - earlephilhower/ESP8266SAM@^1.0.1 - lewisxhe/SensorLib@0.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 + # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library + adafruit/Adafruit DRV2605 Library@1.2.4 + # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio + earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM + earlephilhower/ESP8266SAM@1.1.0 diff --git a/variants/esp32s3/t-watch-s3/variant.h b/variants/esp32s3/t-watch-s3/variant.h index 86b0a03c8..df275c31d 100644 --- a/variants/esp32s3/t-watch-s3/variant.h +++ b/variants/esp32s3/t-watch-s3/variant.h @@ -20,6 +20,8 @@ #define SCREEN_TRANSITION_FRAMERATE 5 // fps #define USE_TFTDISPLAY 1 +#define HAS_DRV2605 1 + #define HAS_TOUCHSCREEN 1 #define SCREEN_TOUCH_INT 16 #define SCREEN_TOUCH_USE_I2C1 @@ -41,16 +43,22 @@ #define HAS_AXP2101 -#define HAS_RTC 1 +// PCF8563 RTC Module +#define PCF8563_RTC 0x51 #define I2C_SDA 10 // For QMC6310 sensors and screens #define I2C_SCL 11 // For QMC6310 sensors and screens +#define HAS_BMA423 1 #define BMA4XX_INT 14 // Interrupt for BMA_423 axis sensor -#define HAS_GPS 0 -#undef GPS_RX_PIN -#undef GPS_TX_PIN +#define HAS_GPS 1 +#define GPS_DEFAULT_NOT_PRESENT 1 +#define GPS_BAUDRATE 38400 +#define GPS_RX_PIN 41 +#define GPS_TX_PIN 42 + +#define BUTTON_PIN 0 // only for Plus version #define USE_SX1262 #define USE_SX1268 diff --git a/variants/esp32s3/tbeam-s3-core/platformio.ini b/variants/esp32s3/tbeam-s3-core/platformio.ini index fba8e4003..512cf3202 100644 --- a/variants/esp32s3/tbeam-s3-core/platformio.ini +++ b/variants/esp32s3/tbeam-s3-core/platformio.ini @@ -1,5 +1,16 @@ ; The 1.0 release of the LilyGo TBEAM-S3-Core board [env:tbeam-s3-core] +custom_meshtastic_hw_model = 12 +custom_meshtastic_hw_model_slug = LILYGO_TBEAM_S3_CORE +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Beam Supreme +custom_meshtastic_images = tbeam-s3-core.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = tbeam-s3-core board_build.partitions = default_8MB.csv @@ -7,9 +18,9 @@ board_check = true lib_deps = ${esp32s3_base.lib_deps} - lewisxhe/PCF8563_Library@1.0.1 + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/tbeam-s3-core - -D PCF8563_RTC=0x51 ;Putting definitions in variant.h does not compile correctly diff --git a/variants/esp32s3/tbeam-s3-core/variant.h b/variants/esp32s3/tbeam-s3-core/variant.h index 1d99fbf14..9ce4aade9 100644 --- a/variants/esp32s3/tbeam-s3-core/variant.h +++ b/variants/esp32s3/tbeam-s3-core/variant.h @@ -53,7 +53,8 @@ // #define PMU_IRQ 40 #define HAS_AXP2101 -#define HAS_RTC 1 +// PCF8563 RTC Module +#define PCF8563_RTC 0x51 // Specify the PMU as Wire1. In the t-beam-s3 core, PCF8563 and PMU share the bus #define PMU_USE_WIRE1 @@ -72,9 +73,6 @@ #define HAS_SDCARD // Have SPI interface SD card slot #define SDCARD_USE_SPI1 -// PCF8563 RTC Module -// #define PCF8563_RTC 0x51 //Putting definitions in variant. h does not compile correctly - // has 32768 Hz crystal #define HAS_32768HZ 1 diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini index d63537904..b5c9fd579 100644 --- a/variants/esp32s3/tlora-pager/platformio.ini +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -1,11 +1,26 @@ ; LilyGo T-Lora-Pager [env:tlora-pager] +custom_meshtastic_hw_model = 103 +custom_meshtastic_hw_model_slug = T_LORA_PAGER +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-LoRa Pager +custom_meshtastic_images = lilygo-tlora-pager.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = esp32s3_base board = t-deck-pro ; same as T-Deck Pro board_check = true board_build.partitions = default_16MB.csv upload_protocol = esptool +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/tlora-pager> + build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/tlora-pager -D T_LORA_PAGER @@ -17,14 +32,23 @@ build_flags = ${esp32s3_base.build_flags} -D ROTARY_BUXTRONICS lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@1.2.7 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 + # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio earlephilhower/ESP8266Audio@1.9.9 - earlephilhower/ESP8266SAM@1.0.1 + # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM + earlephilhower/ESP8266SAM@1.1.0 + # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library adafruit/Adafruit DRV2605 Library@1.2.4 + # renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library lewisxhe/PCF8563_Library@1.0.1 - lewisxhe/SensorLib@0.3.1 - https://github.com/pschatzmann/arduino-audio-driver/archive/refs/tags/v0.1.3.zip + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 + # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver + https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip + # TODO renovate https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip + # TODO renovate https://github.com/mverch67/RotaryEncoder/archive/da958a21389cbcd485989705df602a33e092dd88.zip [env:tlora-pager-tft] diff --git a/variants/esp32s3/tlora-pager/variant.cpp b/variants/esp32s3/tlora-pager/variant.cpp new file mode 100644 index 000000000..7b0cbdfec --- /dev/null +++ b/variants/esp32s3/tlora-pager/variant.cpp @@ -0,0 +1,31 @@ +#include "variant.h" +#include "ExtensionIOXL9555.hpp" +extern ExtensionIOXL9555 io; + +void earlyInitVariant() +{ + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(TFT_CS, OUTPUT); + digitalWrite(TFT_CS, HIGH); + pinMode(KB_INT, INPUT_PULLUP); + // io expander + io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL); + io.pinMode(EXPANDS_DRV_EN, OUTPUT); + io.digitalWrite(EXPANDS_DRV_EN, HIGH); + io.pinMode(EXPANDS_AMP_EN, OUTPUT); + io.digitalWrite(EXPANDS_AMP_EN, LOW); + io.pinMode(EXPANDS_LORA_EN, OUTPUT); + io.digitalWrite(EXPANDS_LORA_EN, HIGH); + io.pinMode(EXPANDS_GPS_EN, OUTPUT); + io.digitalWrite(EXPANDS_GPS_EN, HIGH); + io.pinMode(EXPANDS_KB_EN, OUTPUT); + io.digitalWrite(EXPANDS_KB_EN, HIGH); + io.pinMode(EXPANDS_SD_EN, OUTPUT); + io.digitalWrite(EXPANDS_SD_EN, HIGH); + io.pinMode(EXPANDS_GPIO_EN, OUTPUT); + io.digitalWrite(EXPANDS_GPIO_EN, HIGH); + io.pinMode(EXPANDS_SD_PULLEN, INPUT); +} \ No newline at end of file diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h index fe563cded..d97f864c3 100644 --- a/variants/esp32s3/tlora-pager/variant.h +++ b/variants/esp32s3/tlora-pager/variant.h @@ -21,10 +21,13 @@ #define SCREEN_TRANSITION_FRAMERATE 5 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness #define USE_TFTDISPLAY 1 +#define HAS_PHYSICAL_KEYBOARD 1 #define I2C_SDA SDA #define I2C_SCL SCL +#define HAS_DRV2605 1 + #define USE_POWERSAVE #define SLEEP_TIME 120 @@ -35,12 +38,8 @@ #define GPS_TX_PIN 12 #define PIN_GPS_PPS 13 -// PCF8563 RTC Module -#if __has_include("pcf8563.h") -#include "pcf8563.h" -#endif -#define PCF8563_RTC 0x51 -#define HAS_RTC 1 +// PCF85063 RTC Module +#define PCF85063_RTC 0x51 // Rotary #define ROTARY_A (40) @@ -61,7 +60,6 @@ #define I2C_NO_RESCAN #define KB_BL_PIN 46 #define KB_INT 6 -#define CANNED_MESSAGE_MODULE_ENABLE 1 // audio codec ES8311 #define HAS_I2S diff --git a/variants/esp32s3/tlora_t3s3_epaper/platformio.ini b/variants/esp32s3/tlora_t3s3_epaper/platformio.ini index 82bab453d..256cdc0d0 100644 --- a/variants/esp32s3/tlora_t3s3_epaper/platformio.ini +++ b/variants/esp32s3/tlora_t3s3_epaper/platformio.ini @@ -1,11 +1,21 @@ [env:tlora-t3s3-epaper] +custom_meshtastic_hw_model = 16 +custom_meshtastic_hw_model_slug = TLORA_T3_S3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-LoRa T3-S3 E-Ink +custom_meshtastic_images = tlora-t3s3-epaper.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = true + extends = esp32s3_base board = tlora-t3s3-v1 board_check = true upload_protocol = esptool build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D TLORA_T3S3_EPAPER -I variants/esp32s3/tlora_t3s3_epaper -DUSE_EINK @@ -20,7 +30,8 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:tlora-t3s3-epaper-inkhud] extends = esp32s3_base, inkhud @@ -28,7 +39,7 @@ board = tlora-t3s3-v1 board_check = true upload_protocol = esptool build_src_filter = - ${esp32_base.build_src_filter} + ${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = ${esp32s3_base.build_flags} diff --git a/variants/esp32s3/tlora_t3s3_v1/platformio.ini b/variants/esp32s3/tlora_t3s3_v1/platformio.ini index 56ece0d62..95686e417 100644 --- a/variants/esp32s3/tlora_t3s3_v1/platformio.ini +++ b/variants/esp32s3/tlora_t3s3_v1/platformio.ini @@ -1,8 +1,20 @@ [env:tlora-t3s3-v1] +custom_meshtastic_hw_model = 16 +custom_meshtastic_hw_model_slug = TLORA_T3_S3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-LoRa T3-S3 +custom_meshtastic_images = tlora-t3s3-v1.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = true + extends = esp32s3_base board = tlora-t3s3-v1 board_check = true upload_protocol = esptool build_flags = - ${esp32_base.build_flags} -D TLORA_T3S3_V1 -I variants/esp32s3/tlora_t3s3_v1 + ${esp32s3_base.build_flags} + -D TLORA_T3S3_V1 + -I variants/esp32s3/tlora_t3s3_v1 diff --git a/variants/esp32s3/tracksenger/internal/pins_arduino.h b/variants/esp32s3/tracksenger/internal/pins_arduino.h index 1052af961..93fd5d9c2 100644 --- a/variants/esp32s3/tracksenger/internal/pins_arduino.h +++ b/variants/esp32s3/tracksenger/internal/pins_arduino.h @@ -11,10 +11,6 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/tracksenger/internal/variant.h b/variants/esp32s3/tracksenger/internal/variant.h index 2287dfe0b..ba3e281c8 100644 --- a/variants/esp32s3/tracksenger/internal/variant.h +++ b/variants/esp32s3/tracksenger/internal/variant.h @@ -78,7 +78,6 @@ // keyboard changes #define PIN_BUZZER 43 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define INPUTBROKER_MATRIX_TYPE 1 diff --git a/variants/esp32s3/tracksenger/lcd/pins_arduino.h b/variants/esp32s3/tracksenger/lcd/pins_arduino.h index 1052af961..93fd5d9c2 100644 --- a/variants/esp32s3/tracksenger/lcd/pins_arduino.h +++ b/variants/esp32s3/tracksenger/lcd/pins_arduino.h @@ -11,10 +11,6 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/tracksenger/lcd/variant.h b/variants/esp32s3/tracksenger/lcd/variant.h index f42a5b19f..a9bb89d68 100644 --- a/variants/esp32s3/tracksenger/lcd/variant.h +++ b/variants/esp32s3/tracksenger/lcd/variant.h @@ -102,7 +102,6 @@ // keyboard changes #define PIN_BUZZER 43 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define INPUTBROKER_MATRIX_TYPE 1 diff --git a/variants/esp32s3/tracksenger/oled/pins_arduino.h b/variants/esp32s3/tracksenger/oled/pins_arduino.h index 1052af961..93fd5d9c2 100644 --- a/variants/esp32s3/tracksenger/oled/pins_arduino.h +++ b/variants/esp32s3/tracksenger/oled/pins_arduino.h @@ -11,10 +11,6 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/tracksenger/oled/variant.h b/variants/esp32s3/tracksenger/oled/variant.h index 85cc019c4..689864b32 100644 --- a/variants/esp32s3/tracksenger/oled/variant.h +++ b/variants/esp32s3/tracksenger/oled/variant.h @@ -79,7 +79,6 @@ // keyboard changes #define PIN_BUZZER 43 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define INPUTBROKER_MATRIX_TYPE 1 diff --git a/variants/esp32s3/tracksenger/platformio.ini b/variants/esp32s3/tracksenger/platformio.ini index 213a917b1..c006cf835 100644 --- a/variants/esp32s3/tracksenger/platformio.ini +++ b/variants/esp32s3/tracksenger/platformio.ini @@ -1,4 +1,13 @@ [env:tracksenger] +custom_meshtastic_hw_model = 48 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = TrackSenger (small TFT) +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_wireless_tracker board_build.partitions = default_8MB.csv @@ -12,9 +21,19 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 [env:tracksenger-lcd] +custom_meshtastic_hw_model = 48 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = false +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = TrackSenger (big TFT) +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_wireless_tracker board_build.partitions = default_8MB.csv @@ -28,9 +47,18 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 [env:tracksenger-oled] +custom_meshtastic_hw_model = 48 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = TrackSenger (big OLED) +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_wireless_tracker board_build.partitions = default_8MB.csv diff --git a/variants/esp32s3/unphone/pins_arduino.h b/variants/esp32s3/unphone/pins_arduino.h index 74067359f..8a62e3d42 100644 --- a/variants/esp32s3/unphone/pins_arduino.h +++ b/variants/esp32s3/unphone/pins_arduino.h @@ -6,9 +6,6 @@ #define USB_VID 0x16D0 #define USB_PID 0x1178 -#define LED_BUILTIN 13 -#define BUILTIN_LED LED_BUILTIN // backward compatibility - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini index f17a27e17..924dfa74f 100644 --- a/variants/esp32s3/unphone/platformio.ini +++ b/variants/esp32s3/unphone/platformio.ini @@ -1,6 +1,15 @@ ; platformio.ini for unphone meshtastic [env:unphone] +custom_meshtastic_hw_model = 59 +custom_meshtastic_hw_model_slug = UNPHONE +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = unPhone +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = unphone board_build.partitions = partition-table-8MB.csv @@ -27,10 +36,12 @@ build_src_filter = +<../variants/esp32s3/unphone> lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@ 1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.19 + # TODO renovate https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0 - adafruit/Adafruit NeoPixel @ ^1.12.0 - + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 [env:unphone-tft] board_level = extra diff --git a/variants/native/portduino-buildroot/platformio.ini b/variants/native/portduino-buildroot/platformio.ini index a3d0f4639..a3a779aeb 100644 --- a/variants/native/portduino-buildroot/platformio.ini +++ b/variants/native/portduino-buildroot/platformio.ini @@ -5,5 +5,4 @@ extends = portduino_base build_flags = ${portduino_base.build_flags} -O0 -I variants/native/portduino-buildroot board = buildroot board_level = extra -lib_deps = ${portduino_base.lib_deps} build_src_filter = ${portduino_base.build_src_filter} \ No newline at end of file diff --git a/variants/native/portduino-buildroot/variant.h b/variants/native/portduino-buildroot/variant.h index affd83051..ac6421b14 100644 --- a/variants/native/portduino-buildroot/variant.h +++ b/variants/native/portduino-buildroot/variant.h @@ -1,6 +1,5 @@ #define HAS_SCREEN 1 #define USE_TFTDISPLAY 1 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define HAS_GPS 1 #define MAX_RX_TOPHONE portduino_config.maxtophone #define MAX_NUM_NODES portduino_config.MaxNodes diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index bce06f907..b86420291 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -21,18 +21,21 @@ build_src_filter = lib_deps = ${env.lib_deps} ${networking_base.lib_deps} + ${networking_extra.lib_deps} ${radiolib_base.lib_deps} ${environmental_base.lib_deps} # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@^1.2.0 + lovyan03/LovyanGFX@1.2.19 # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main - https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip + https://github.com/pine64/libch341-spi-userspace/archive/23c42319a69cffcb65868e3c72e6bed83974a393.zip # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library adafruit/Adafruit seesaw Library@1.7.9 # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip + # renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680 + adafruit/Adafruit BME680 Library@^2.0.5 build_flags = ${arduino_base.build_flags} diff --git a/variants/native/portduino/platformio.ini b/variants/native/portduino/platformio.ini index 9cedfcc55..045e3edea 100644 --- a/variants/native/portduino/platformio.ini +++ b/variants/native/portduino/platformio.ini @@ -6,7 +6,8 @@ board = cross_platform board_level = extra lib_deps = ${portduino_base.lib_deps} - melopero/Melopero RV3028@^1.1.0 + # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 + melopero/Melopero RV3028@1.2.0 build_src_filter = ${portduino_base.build_src_filter} diff --git a/variants/native/portduino/variant.h b/variants/native/portduino/variant.h index 972443450..cba4aaedd 100644 --- a/variants/native/portduino/variant.h +++ b/variants/native/portduino/variant.h @@ -2,7 +2,6 @@ #define HAS_SCREEN 1 #endif #define USE_TFTDISPLAY 1 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define HAS_GPS 1 #define MAX_RX_TOPHONE portduino_config.maxtophone #define MAX_NUM_NODES portduino_config.MaxNodes diff --git a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini index 83044c206..093c3732d 100644 --- a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini +++ b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini @@ -11,5 +11,6 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/Dongle_nRF52840-pca10059-v1> lib_deps = ${nrf52840_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.6 debug_tool = jlink diff --git a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.h b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.h index 2318450eb..8baf25e87 100644 --- a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.h +++ b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.h @@ -50,9 +50,6 @@ extern "C" { #define RGBLED_BLUE (0 + 12) // Blue of RGB P0.12 #define RGBLED_CA // comment out this line if you have a common cathode type, as defined use common anode logic -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 - #define LED_GREEN PIN_LED1 #define LED_BLUE PIN_LED2 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini index f89b05d1f..a4687669b 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini @@ -1,5 +1,14 @@ ; First prototype eink/nrf52840/sx1262 device [env:thinknode_m1] +custom_meshtastic_hw_model = 89 +custom_meshtastic_hw_model_slug = THINKNODE_M1 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = ThinkNode M1 +custom_meshtastic_images = thinknode_m1.svg +custom_meshtastic_tags = Elecrow + extends = nrf52840_base board = ThinkNode-M1 board_check = true @@ -23,9 +32,10 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M1> lib_deps = ${nrf52840_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip - lewisxhe/PCF8563_Library@^1.0.1 - khoih-prog/nRF52_PWM@^1.0.1 + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + # renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM + khoih-prog/nRF52_PWM@1.0.1 ;upload_protocol = fs [env:thinknode_m1-inkhud] @@ -45,4 +55,3 @@ 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 \ No newline at end of file diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp index cae079b74..04f86e2d4 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp @@ -32,13 +32,31 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - // LED1 & LED2 pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - - pinMode(PIN_LED3, OUTPUT); - ledOff(PIN_LED3); } + +void variant_shutdown() +{ + for (int pin = 0; pin < 48; pin++) { + if (pin == SX126X_BUSY || pin == PIN_SPI_SCK || pin == SX126X_DIO1 || pin == PIN_SPI_MOSI || pin == PIN_SPI_MISO || + pin == SX126X_CS || pin == SX126X_RESET || pin == PIN_NFC1 || pin == PIN_NFC2 || pin == PIN_BUTTON1 || + pin == PIN_BUTTON2) { + continue; + } + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); + if (pin >= 32) { + NRF_P1->DIRCLR = (1 << (pin - 32)); + } else { + NRF_GPIO->DIRCLR = (1 << pin); + } + } + nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input + nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense); + + nrf_gpio_cfg_input(PIN_BUTTON2, NRF_GPIO_PIN_PULLUP); + nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(PIN_BUTTON2, sense1); +} \ No newline at end of file diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h index e4a6c0397..e00e56785 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h @@ -41,22 +41,15 @@ extern "C" { #define NUM_ANALOG_INPUTS (1) #define NUM_ANALOG_OUTPUTS (0) -#define PIN_LED2 -1 -#define PIN_LED3 -1 - // LED #define PIN_LED1 (32 + 6) // red #define LED_POWER (32 + 4) -#define USER_LED (0 + 13) // green +#define LED_NOTIFICATION (0 + 13) // green // USB_CHECK #define EXT_PWR_DETECT (32 + 3) #define ADC_V (0 + 8) -#define LED_RED PIN_LED3 #define LED_BLUE PIN_LED1 -#define LED_GREEN PIN_LED2 -#define LED_BUILTIN LED_BLUE -#define LED_CONN PIN_GREEN #define LED_STATE_ON 0 // State when LED is lit // LED灯亮时的状态 #define PIN_BUZZER (0 + 6) /* @@ -93,8 +86,6 @@ static const uint8_t A0 = PIN_A0; #define TP_SER_IO (0 + 11) -#define PIN_RTC_INT (0 + 16) // Interrupt from the PCF8563 RTC - /* External serial flash WP25R1635FZUIL0 */ @@ -161,8 +152,7 @@ External serial flash WP25R1635FZUIL0 #define PIN_SERIAL1_TX GPS_TX_PIN #define PIN_SERIAL1_RX GPS_RX_PIN -// PCF8563 RTC Module -#define PCF8563_RTC 0x51 +#define SERIAL_PRINT_PORT 0 /* * SPI Interfaces @@ -174,8 +164,6 @@ External serial flash WP25R1635FZUIL0 #define PIN_SPI_MOSI (0 + 22) #define PIN_SPI_SCK (0 + 19) -#define PIN_PWR_EN (0 + 6) - // To debug via the segger JLINK console rather than the CDC-ACM serial device // #define USE_SEGGER @@ -190,7 +178,6 @@ External serial flash WP25R1635FZUIL0 #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (2.02F) -// #define HAS_RTC 0 // #define HAS_SCREEN 0 #ifdef __cplusplus diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini index 958e48e48..7a87c2a21 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini @@ -1,8 +1,17 @@ [env:thinknode_m3] +custom_meshtastic_support_level = 1 +custom_meshtastic_images = thinknode_m3.svg +custom_meshtastic_tags = Elecrow + extends = nrf52840_base board = ThinkNode-M3 board_check = true debug_tool = jlink +custom_meshtastic_hw_model = 115 +custom_meshtastic_hw_model_slug = THINKNODE_M3 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_display_name = Elecrow ThinkNode M3 +custom_meshtastic_actively_supported = true build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/ELECROW-ThinkNode-M3 @@ -13,5 +22,7 @@ build_flags = build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M3> lib_deps = ${nrf52840_base.lib_deps} - khoih-prog/nRF52_PWM@^1.0.1 - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM + khoih-prog/nRF52_PWM@1.0.1 + ; # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp index b7a7b7342..45a64ad3b 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp @@ -37,8 +37,8 @@ void initVariant() digitalWrite(KEY_POWER, HIGH); pinMode(RGB_POWER, OUTPUT); digitalWrite(RGB_POWER, HIGH); - pinMode(green_LED_PIN, OUTPUT); - digitalWrite(green_LED_PIN, LED_STATE_OFF); + pinMode(LED_GREEN, OUTPUT); + digitalWrite(LED_GREEN, LED_STATE_OFF); pinMode(LED_BLUE, OUTPUT); pinMode(PIN_POWER_USB, INPUT); pinMode(PIN_POWER_DONE, INPUT); @@ -63,15 +63,26 @@ void initVariant() // called from main-nrf52.cpp during the cpuDeepSleep() function void variant_shutdown() { + digitalWrite(LED_RED, HIGH); + digitalWrite(LED_GREEN, HIGH); + digitalWrite(LED_BLUE, HIGH); + + digitalWrite(PIN_EN1, LOW); + digitalWrite(PIN_EN2, LOW); digitalWrite(EEPROM_POWER, LOW); digitalWrite(KEY_POWER, LOW); + digitalWrite(DHT_POWER, LOW); + digitalWrite(ACC_POWER, LOW); + digitalWrite(Battery_POWER, LOW); + digitalWrite(GPS_POWER, LOW); + // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. for (int pin = 0; pin < 48; pin++) { if (pin == PIN_POWER_USB || pin == BUTTON_PIN || pin == PIN_EN1 || pin == PIN_EN2 || pin == DHT_POWER || pin == ACC_POWER || pin == Battery_POWER || pin == GPS_POWER || pin == LR1110_SPI_MISO_PIN || pin == LR1110_SPI_MOSI_PIN || pin == LR1110_SPI_SCK_PIN || pin == LR1110_SPI_NSS_PIN || pin == LR1110_BUSY_PIN || - pin == LR1110_NRESET_PIN || pin == LR1110_IRQ_PIN || pin == GPS_TX_PIN || pin == GPS_RX_PIN || pin == green_LED_PIN || - pin == red_LED_PIN || pin == LED_BLUE) { + pin == LR1110_NRESET_PIN || pin == LR1110_IRQ_PIN || pin == GPS_TX_PIN || pin == GPS_RX_PIN || pin == LED_GREEN || + pin == LED_RED || pin == LED_BLUE) { continue; } pinMode(pin, OUTPUT); diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h index 2ad3efa27..50944b6d7 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h @@ -50,14 +50,14 @@ extern "C" { #define EEPROM_POWER 7 // LED -#define red_LED_PIN 33 -#define LED_POWER red_LED_PIN +#define LED_RED 33 +#define LED_POWER LED_RED #define LED_CHARGE LED_POWER // Signals the Status LED Module to handle this LED -#define green_LED_PIN 35 +#define LED_GREEN 35 +#define LED_NOTIFICATION LED_GREEN #define LED_BLUE 37 #define LED_PAIRING LED_BLUE // Signals the Status LED Module to handle this LED -#define LED_BUILTIN -1 #define LED_STATE_ON LOW #define LED_STATE_OFF HIGH @@ -112,8 +112,11 @@ extern "C" { #define LR11X0_DIO3_TCXO_VOLTAGE 3.3 #define LR11X0_DIO_AS_RF_SWITCH +#define SERIAL_PRINT_PORT 0 + // PCF8563 RTC Module #define PCF8563_RTC 0x51 +#define HAS_RTC 1 #ifdef __cplusplus } diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini new file mode 100644 index 000000000..9a2b3a467 --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini @@ -0,0 +1,15 @@ +; ThinkNode M4 - Powerbank nrf52840/LR1110 by Elecrow +[env:thinknode_m4] +extends = nrf52840_base +board = ThinkNode-M4 +board_check = true +debug_tool = jlink + +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/ELECROW-ThinkNode-M4 + -DELECROW_ThinkNode_M4 + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M4> +lib_deps = + ${nrf52840_base.lib_deps} + lewisxhe/PCF8563_Library@^1.0.1 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/rfswitch.h b/variants/nrf52840/ELECROW-ThinkNode-M4/rfswitch.h new file mode 100644 index 000000000..e5fe182c4 --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/rfswitch.h @@ -0,0 +1,11 @@ +#include "RadioLib.h" + +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, +}; diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp new file mode 100644 index 000000000..999f326db --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp @@ -0,0 +1,66 @@ +/* + 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() +{ + pinMode(LED_PAIRING, OUTPUT); + ledOff(LED_PAIRING); + + pinMode(Battery_LED_1, OUTPUT); + ledOff(Battery_LED_1); + pinMode(Battery_LED_2, OUTPUT); + ledOff(Battery_LED_2); + + pinMode(Battery_LED_3, OUTPUT); + ledOff(Battery_LED_3); + + pinMode(Battery_LED_4, OUTPUT); + ledOff(Battery_LED_4); +} + +/// called from main-nrf52.cpp during the cpuDeepSleep() function +void variant_shutdown() +{ + for (int pin = 0; pin < 48; pin++) { + if (pin == PIN_GPS_EN || pin == PIN_BUTTON1) { + continue; + } + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); + if (pin >= 32) { + NRF_P1->DIRCLR = (1 << (pin - 32)); + } else { + NRF_GPIO->DIRCLR = (1 << pin); + } + } + digitalWrite(PIN_GPS_EN, HIGH); +} diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h new file mode 100644 index 000000000..2cfe948e3 --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h @@ -0,0 +1,143 @@ +/* + 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_ELECROW_THINKNODE_M4_ +#define _VARIANT_ELECROW_THINKNODE_M4_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define LED_BLUE -1 +#define LED_NOTIFICATION (32 + 9) +#define LED_PAIRING (13) + +#define Battery_LED_1 (15) +#define Battery_LED_2 (17) +#define Battery_LED_3 (32 + 2) +#define Battery_LED_4 (32 + 4) + +#define LED_STATE_ON 1 + +// Button +#define PIN_BUTTON1 (4) + +// Battery ADC +#define PIN_A0 (2) +#define BATTERY_PIN PIN_A0 +#define BATTERY_SENSE_SAMPLES 30 +#define ADC_RESOLUTION 14 +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#define ADC_MULTIPLIER (2.00F) +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 + +#define HAS_SERIAL_BATTERY_LEVEL 1 +#define SERIAL_BATTERY_RX 30 +#define SERIAL_BATTERY_TX 5 + +static const uint8_t A0 = PIN_A0; + +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +// I2C +#define WIRE_INTERFACES_COUNT 1 +#define PIN_WIRE_SDA (23) +#define PIN_WIRE_SCL (25) + +// actually the LORA Radio +#define PIN_POWER_EN (11) + +// charger status +#define EXT_CHRG_DETECT (32 + 6) +#define EXT_CHRG_DETECT_VALUE HIGH + +// SPI +#define SPI_INTERFACES_COUNT 1 +#define PIN_SPI_MISO (8) +#define PIN_SPI_MOSI (7) +#define PIN_SPI_SCK (6) + +#define LORA_RESET (32 + 8) +#define LORA_DIO1 (12) +#define LORA_DIO2 (26) +#define LORA_SCK PIN_SPI_SCK +#define LORA_MISO PIN_SPI_MISO +#define LORA_MOSI PIN_SPI_MOSI +#define LORA_CS (27) + +#define USE_LR1110 +#define LR1110_IRQ_PIN LORA_DIO1 +#define LR1110_NRESET_PIN LORA_RESET +#define LR1110_BUSY_PIN LORA_DIO2 +#define LR1110_SPI_NSS_PIN LORA_CS +#define LR1110_SPI_SCK_PIN LORA_SCK +#define LR1110_SPI_MOSI_PIN LORA_MOSI +#define LR1110_SPI_MISO_PIN LORA_MISO + +#define LR11X0_DIO3_TCXO_VOLTAGE 1.6 +#define LR11X0_DIO_AS_RF_SWITCH + +// Peripherals on I2C bus. Active Low +#define VEXT_ENABLE (32) +#define VEXT_ON_VALUE LOW + +// GPS L76K +#define HAS_GPS 1 +#define GPS_L76K +#define GPS_BAUDRATE 9600 +#define PIN_GPS_EN (32 + 11) +#define GPS_EN_ACTIVE LOW +#define PIN_GPS_RESET (3) +#define GPS_RESET_MODE HIGH +#define PIN_GPS_STANDBY (28) +#define GPS_STANDBY_ACTIVE HIGH +#define GPS_TX_PIN (32 + 12) +#define GPS_RX_PIN (32 + 14) +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN + +#define SERIAL_PRINT_PORT 0 + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini index 2bf227791..329111ce6 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini @@ -1,5 +1,14 @@ ; ThinkNode M6 - Outdoor Solar Power nrf52840/sx1262 device [env:thinknode_m6] +custom_meshtastic_hw_model = 120 +custom_meshtastic_hw_model_slug = THINKNODE_M6 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = false +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = ThinkNode M6 +custom_meshtastic_images = thinknode_m6.svg +custom_meshtastic_tags = Elecrow + extends = nrf52840_base board = ThinkNode-M6 board_check = true @@ -12,4 +21,5 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M6> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 + ; # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp index 09872d409..bc0381a48 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp @@ -41,3 +41,34 @@ void initVariant() pinMode(VDD_FLASH_EN, OUTPUT); digitalWrite(VDD_FLASH_EN, HIGH); } + +// called from main-nrf52.cpp during the cpuDeepSleep() function +void variant_shutdown() +{ + // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. + for (int pin = 0; pin < 48; pin++) { + if (pin == PIN_GPS_EN || pin == ADC_CTRL || pin == PIN_BUTTON1 || pin == PIN_SPI_MISO || pin == PIN_SPI_MOSI || + pin == PIN_SPI_SCK || pin == SX126X_CS || pin == SX126X_RESET || pin == SX126X_BUSY || pin == SX126X_DIO1) { + continue; + } + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); + if (pin >= 32) { + NRF_P1->DIRCLR = (1 << (pin - 32)); + } else { + NRF_GPIO->DIRCLR = (1 << pin); + } + } + + digitalWrite(PIN_GPS_EN, LOW); + digitalWrite(ADC_CTRL, LOW); + // digitalWrite(RTC_POWER, LOW); + + nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input + nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense1); + + nrf_gpio_cfg_input(EXT_CHRG_DETECT, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input + nrf_gpio_pin_sense_t sense2 = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(EXT_CHRG_DETECT, sense2); +} diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h index d30b88d66..ba6aa14ab 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h @@ -40,12 +40,13 @@ extern "C" { #define NUM_ANALOG_OUTPUTS (0) // LEDs -#define LED_BUILTIN -1 #define LED_BLUE -1 #define LED_CHARGE (12) #define LED_PAIRING (7) +#define LED_NOTIFICATION LED_PAIRING -#define LED_STATE_ON 1 +#define LED_STATE_ON HIGH +#define LED_STATE_OFF LOW // USB power detection #define EXT_PWR_DETECT (13) @@ -120,6 +121,7 @@ static const uint8_t A0 = PIN_A0; // PCF8563 RTC Module #define PCF8563_RTC 0x51 +#define HAS_RTC 1 // SPI #define SPI_INTERFACES_COUNT 1 @@ -131,12 +133,14 @@ static const uint8_t A0 = PIN_A0; #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 AREF_VOLTAGE 2.4 +#define VBAT_AR_INTERNAL AR_INTERNAL_2_4 #define ADC_MULTIPLIER (1.75F) #define HAS_SOLAR +#define OCV_ARRAY 4080, 3990, 3935, 3880, 3825, 3770, 3715, 3660, 3605, 3550, 3450 + #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/ME25LS01-4Y10TD/platformio.ini b/variants/nrf52840/ME25LS01-4Y10TD/platformio.ini index 1279f12c6..ef2e6e517 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD/platformio.ini +++ b/variants/nrf52840/ME25LS01-4Y10TD/platformio.ini @@ -10,8 +10,6 @@ build_flags = ${nrf52840_base.build_flags} -DME25LS01_4Y10TD board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ME25LS01-4Y10TD> -lib_deps = - ${nrf52840_base.lib_deps} ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) upload_protocol = nrfutil ;upload_port = /dev/ttyACM1 diff --git a/variants/nrf52840/ME25LS01-4Y10TD/variant.h b/variants/nrf52840/ME25LS01-4Y10TD/variant.h index e772069da..a920b02e5 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD/variant.h +++ b/variants/nrf52840/ME25LS01-4Y10TD/variant.h @@ -51,7 +51,6 @@ extern "C" { #define PIN_LED1 (32 + 7) // P1.07 Blue D2 #define LED_PIN PIN_LED1 -#define LED_BUILTIN -1 #define LED_BLUE -1 #define LED_STATE_ON 1 // State when LED is lit diff --git a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini index f8d6da008..5951a37a3 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini @@ -15,7 +15,8 @@ board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ME25LS01-4Y10TD_e-ink> lib_deps = ${nrf52840_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.6 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) upload_protocol = nrfutil ;upload_port = /dev/ttyACM1 diff --git a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.h b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.h index 797394ce6..683669160 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.h +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.h @@ -51,7 +51,6 @@ extern "C" { #define PIN_LED1 (32 + 7) // P1.07 Blue D2 #define LED_PIN PIN_LED1 -#define LED_BUILTIN -1 #define LED_BLUE -1 #define LED_STATE_ON 1 // State when LED is lit diff --git a/variants/nrf52840/MS24SF1/platformio.ini b/variants/nrf52840/MS24SF1/platformio.ini index df15b5605..1cbb3a5a5 100644 --- a/variants/nrf52840/MS24SF1/platformio.ini +++ b/variants/nrf52840/MS24SF1/platformio.ini @@ -9,8 +9,6 @@ build_flags = ${nrf52840_base.build_flags} -Isrc/platform/nrf52/softdevice/nrf52 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/MS24SF1> -lib_deps = - ${nrf52840_base.lib_deps} ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) upload_protocol = nrfutil ;upload_port = /dev/ttyACM1 diff --git a/variants/nrf52840/MS24SF1/variant.h b/variants/nrf52840/MS24SF1/variant.h index d26dcebc2..a71be99fd 100644 --- a/variants/nrf52840/MS24SF1/variant.h +++ b/variants/nrf52840/MS24SF1/variant.h @@ -51,7 +51,6 @@ extern "C" { #define PIN_LED1 (-1) #define LED_PIN PIN_LED1 -#define LED_BUILTIN -1 #define LED_BLUE -1 #define LED_STATE_ON 1 // State when LED is lit diff --git a/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini b/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini index 50e5495f0..ecb630bb7 100644 --- a/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini +++ b/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini @@ -12,7 +12,9 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/MakePython_nRF52840_eink> lib_deps = ${nrf52840_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip - zinggjm/GxEPD2@^1.6.2 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.6 debug_tool = jlink ;upload_port = /dev/ttyACM4 \ No newline at end of file diff --git a/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp b/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp index 8c6bf039c..04cda84ac 100644 --- a/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp +++ b/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp @@ -32,7 +32,4 @@ void initVariant() // LED1 & LED2 pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); } diff --git a/variants/nrf52840/MakePython_nRF52840_eink/variant.h b/variants/nrf52840/MakePython_nRF52840_eink/variant.h index 00c8dc199..64cca4916 100644 --- a/variants/nrf52840/MakePython_nRF52840_eink/variant.h +++ b/variants/nrf52840/MakePython_nRF52840_eink/variant.h @@ -27,15 +27,10 @@ extern "C" { // LEDs #define PIN_LED1 (32 + 10) // LED P1.15 -#define PIN_LED2 (-1) // - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 -#define LED_STATE_ON 0 // State when LED is litted +#define LED_STATE_ON 0 // State when LED is lit /* * Buttons diff --git a/variants/nrf52840/MakePython_nRF52840_oled/platformio.ini b/variants/nrf52840/MakePython_nRF52840_oled/platformio.ini index c7418e53c..16cef8964 100644 --- a/variants/nrf52840/MakePython_nRF52840_oled/platformio.ini +++ b/variants/nrf52840/MakePython_nRF52840_oled/platformio.ini @@ -8,5 +8,6 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/MakePython_nRF52840_oled> lib_deps = ${nrf52840_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip debug_tool = jlink diff --git a/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp b/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp index 8c6bf039c..04cda84ac 100644 --- a/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp +++ b/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp @@ -32,7 +32,4 @@ void initVariant() // LED1 & LED2 pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); } diff --git a/variants/nrf52840/MakePython_nRF52840_oled/variant.h b/variants/nrf52840/MakePython_nRF52840_oled/variant.h index 28d941171..2f035e7da 100644 --- a/variants/nrf52840/MakePython_nRF52840_oled/variant.h +++ b/variants/nrf52840/MakePython_nRF52840_oled/variant.h @@ -27,15 +27,10 @@ extern "C" { // LEDs #define PIN_LED1 (32 + 10) // LED P1.15 -#define PIN_LED2 (-1) // - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 -#define LED_STATE_ON 0 // State when LED is litted +#define LED_STATE_ON 0 // State when LED is lit /* * Buttons diff --git a/variants/nrf52840/TWC_mesh_v4/platformio.ini b/variants/nrf52840/TWC_mesh_v4/platformio.ini index 77aeee26e..d93f179c2 100644 --- a/variants/nrf52840/TWC_mesh_v4/platformio.ini +++ b/variants/nrf52840/TWC_mesh_v4/platformio.ini @@ -8,5 +8,6 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/TWC_mesh_v4> lib_deps = ${nrf52840_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.6 debug_tool = jlink diff --git a/variants/nrf52840/TWC_mesh_v4/variant.h b/variants/nrf52840/TWC_mesh_v4/variant.h index 6a6f541e6..875040b5f 100644 --- a/variants/nrf52840/TWC_mesh_v4/variant.h +++ b/variants/nrf52840/TWC_mesh_v4/variant.h @@ -34,9 +34,6 @@ extern "C" { // #define PIN_LED1 (32 + 9) Green // #define PIN_LED1 (0 + 12) Blue -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 - #define LED_GREEN PIN_LED1 #define LED_BLUE PIN_LED2 diff --git a/variants/nrf52840/canaryone/platformio.ini b/variants/nrf52840/canaryone/platformio.ini index 251937e9c..a2cf55972 100644 --- a/variants/nrf52840/canaryone/platformio.ini +++ b/variants/nrf52840/canaryone/platformio.ini @@ -1,5 +1,13 @@ ; Public Beta oled/nrf52840/sx1262 device [env:canaryone] +custom_meshtastic_hw_model = 29 +custom_meshtastic_hw_model_slug = CANARYONE +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Canary One +custom_meshtastic_tags = Canary + extends = nrf52840_base board = canaryone debug_tool = jlink @@ -11,5 +19,4 @@ build_flags = build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/canaryone> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 ;upload_protocol = fs diff --git a/variants/nrf52840/canaryone/variant.h b/variants/nrf52840/canaryone/variant.h index 204ca6306..3f45b618a 100644 --- a/variants/nrf52840/canaryone/variant.h +++ b/variants/nrf52840/canaryone/variant.h @@ -52,9 +52,6 @@ extern "C" { #define LED_BLUE PIN_LED1 -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED3 - #define LED_STATE_ON 0 // State when LED is lit /* @@ -103,7 +100,7 @@ static const uint8_t A0 = PIN_A0; #define EXTERNAL_FLASH_USE_QSPI // Add a delay on startup to allow LoRa and GPS to power up -#define PIN_PWR_DELAY_MS 100 +#define PERIPHERAL_WARMUP_MS 100 /* * Lora radio @@ -170,6 +167,8 @@ static const uint8_t A0 = PIN_A0; #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (2.0F) +#define SERIAL_PRINT_PORT 0 + #ifdef __cplusplus } #endif @@ -178,4 +177,4 @@ static const uint8_t A0 = PIN_A0; * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/diy/WashTastic/platformio.ini b/variants/nrf52840/diy/WashTastic/platformio.ini index 881b961e1..0e67f72ad 100644 --- a/variants/nrf52840/diy/WashTastic/platformio.ini +++ b/variants/nrf52840/diy/WashTastic/platformio.ini @@ -8,6 +8,4 @@ build_flags = ${nrf52840_base.build_flags} -D PRIVATE_HW -D EBYTE_E22_900M30S build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/diy/nrf52_promicro_diy_tcxo> -lib_deps = - ${nrf52840_base.lib_deps} debug_tool = jlink diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini index 61a6eda07..82a4e1953 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini @@ -1,13 +1,21 @@ ; Promicro + E22(0)-xxxM / HT-RA62 modules board variant - DIY - with TCXO [env:nrf52_promicro_diy_tcxo] +custom_meshtastic_hw_model = 63 +custom_meshtastic_hw_model_slug = NRF52_PROMICRO_DIY +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = NRF52 Pro-micro DIY +custom_meshtastic_images = promicro.svg +custom_meshtastic_tags = DIY +custom_meshtastic_requires_dfu = true + extends = nrf52840_base board = promicro-nrf52840 build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/diy/nrf52_promicro_diy_tcxo -D NRF52_PROMICRO_DIY build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/diy/nrf52_promicro_diy_tcxo> -lib_deps = - ${nrf52840_base.lib_deps} debug_tool = jlink ; NRF52 ProMicro w/ E-Ink display @@ -28,5 +36,5 @@ lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${nrf52840_base.lib_deps} extra_scripts = - ${env.extra_scripts} + ${nrf52840_base.extra_scripts} variants/nrf52840/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py ; Add to PIO's Project Tasks pane: preset builds for common displays diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp index 5869ed1d4..384c618e2 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp @@ -36,3 +36,10 @@ void initVariant() pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } + +void variant_shutdown() +{ + nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Enable internal pull-up on the button pin + nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; // Configure SENSE signal on low edge + nrf_gpio_cfg_sense_set(BUTTON_PIN, sense); // Apply SENSE to wake up the device from the deep sleep +} diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h index 7eeb26e65..8e10141f5 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h @@ -81,7 +81,6 @@ NRF52 PRO MICRO PIN ASSIGNMENT // LED #define PIN_LED1 (0 + 15) // P0.15 -#define LED_BUILTIN PIN_LED1 // Actually red #define LED_BLUE PIN_LED1 #define LED_STATE_ON 1 // State when LED is lit @@ -90,16 +89,16 @@ NRF52 PRO MICRO PIN ASSIGNMENT #define BUTTON_PIN (32 + 0) // P1.00 // GPS -#define PIN_GPS_TX (0 + 20) // P0.20 - This is data from the MCU -#define PIN_GPS_RX (0 + 22) // P0.22 - This is data from the GNSS +#define GPS_TX_PIN (0 + 20) // P0.20 - This is data from the MCU +#define GPS_RX_PIN (0 + 22) // P0.22 - This is data from the GNSS #define PIN_GPS_EN (0 + 24) // P0.24 #define GPS_UBLOX // define GPS_DEBUG // UART interfaces -#define PIN_SERIAL1_TX PIN_GPS_TX -#define PIN_SERIAL1_RX PIN_GPS_RX +#define PIN_SERIAL1_TX GPS_TX_PIN +#define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL2_RX (0 + 6) // P0.06 #define PIN_SERIAL2_TX (0 + 8) // P0.08 diff --git a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/platformio.ini b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/platformio.ini index 2df31d23c..10eab2aa4 100644 --- a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/platformio.ini +++ b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/platformio.ini @@ -10,6 +10,4 @@ build_flags = ${nrf52840_base.build_flags} -Isrc/platform/nrf52/softdevice/nrf52 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262> -lib_deps = - ${nrf52840_base.lib_deps} debug_tool = jlink diff --git a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h index 277377d71..c132eba5b 100644 --- a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h +++ b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h @@ -35,10 +35,6 @@ extern "C" { #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 // XIAO Wio-SX1262 Shield User button diff --git a/variants/nrf52840/dls_Minimesh_Lite/platformio.ini b/variants/nrf52840/dls_Minimesh_Lite/platformio.ini new file mode 100644 index 000000000..763ff5477 --- /dev/null +++ b/variants/nrf52840/dls_Minimesh_Lite/platformio.ini @@ -0,0 +1,9 @@ +[env:minimesh_lite] +extends = nrf52840_base +board = minimesh_lite +board_level = extra +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/dls_Minimesh_Lite + -DPRIVATE_HW +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/dls_Minimesh_Lite> +debug_tool = jlink diff --git a/variants/nrf52840/dls_Minimesh_Lite/variant.cpp b/variants/nrf52840/dls_Minimesh_Lite/variant.cpp new file mode 100644 index 000000000..5869ed1d4 --- /dev/null +++ b/variants/nrf52840/dls_Minimesh_Lite/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 + 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() +{ + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} diff --git a/variants/nrf52840/dls_Minimesh_Lite/variant.h b/variants/nrf52840/dls_Minimesh_Lite/variant.h new file mode 100644 index 000000000..32c16f06d --- /dev/null +++ b/variants/nrf52840/dls_Minimesh_Lite/variant.h @@ -0,0 +1,103 @@ +#ifndef _VARIANT_MINIMESH_LITE_ +#define _VARIANT_MINIMESH_LITE_ + +#define VARIANT_MCK (64000000ul) +#define USE_LFRC + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define MINIMESH_LITE + +// 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) + +#define PIN_3V3_EN (0 + 13) // P0.13 + +// Analog pins +#define BATTERY_PIN (0 + 31) // P0.31 Battery ADC +#define ADC_CHANNEL ADC1_GPIO4_CHANNEL +#define ADC_RESOLUTION 14 +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#define VBAT_MV_PER_LSB (0.73242188F) +#define VBAT_DIVIDER (0.6F) +#define VBAT_DIVIDER_COMP (1.73) +#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB) +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER VBAT_DIVIDER_COMP +#define VBAT_RAW_TO_SCALED(x) (REAL_VBAT_MV_PER_LSB * x) + +// WIRE IC AND IIC PINS +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (32 + 4) +#define PIN_WIRE_SCL (0 + 11) + +// LED +#define PIN_LED1 (0 + 15) +// Actually red +#define LED_BLUE PIN_LED1 +#define LED_STATE_ON 1 + +// Button +#define BUTTON_PIN (32 + 0) + +// GPS +#define GPS_TX_PIN (0 + 20) +#define GPS_RX_PIN (0 + 22) + +#define PIN_GPS_EN (0 + 24) +#define GPS_UBLOX +// define GPS_DEBUG + +// UART interfaces +#define PIN_SERIAL1_TX GPS_TX_PIN +#define PIN_SERIAL1_RX GPS_RX_PIN + +#define PIN_SERIAL2_RX (0 + 6) +#define PIN_SERIAL2_TX (0 + 8) + +// Serial interfaces +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (0 + 2) +#define PIN_SPI_MOSI (32 + 15) +#define PIN_SPI_SCK (32 + 11) + +#define LORA_MISO PIN_SPI_MISO +#define LORA_MOSI PIN_SPI_MOSI +#define LORA_SCK PIN_SPI_SCK +#define LORA_CS (32 + 13) + +// LORA MODULES +#define USE_LLCC68 +#define USE_SX1262 +#define USE_SX1268 + +// SX126X CONFIG +#define SX126X_CS (32 + 13) +#define SX126X_DIO1 (0 + 10) +#define SX126X_DIO2_AS_RF_SWITCH + +#define SX126X_BUSY (0 + 29) +#define SX126X_RESET (0 + 9) +#define SX126X_RXEN (0 + 17) +#define SX126X_TXEN RADIOLIB_NC + +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL + +#ifdef __cplusplus +} +#endif + +#endif // _VARIANT_MINIMESH_LITE_ diff --git a/variants/nrf52840/feather_diy/platformio.ini b/variants/nrf52840/feather_diy/platformio.ini index a17e418a2..5540021b3 100644 --- a/variants/nrf52840/feather_diy/platformio.ini +++ b/variants/nrf52840/feather_diy/platformio.ini @@ -6,8 +6,6 @@ build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/feather_diy -Dfeather_diy build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/feather_diy> -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 \ No newline at end of file diff --git a/variants/nrf52840/feather_diy/variant.h b/variants/nrf52840/feather_diy/variant.h index 1c0979f82..a816e7867 100644 --- a/variants/nrf52840/feather_diy/variant.h +++ b/variants/nrf52840/feather_diy/variant.h @@ -49,12 +49,10 @@ extern "C" { #define PIN_LED1 (32 + 15) // P1.15 3 #define PIN_LED2 (32 + 10) // P1.10 4 -#define LED_BUILTIN PIN_LED1 - #define LED_GREEN PIN_LED2 // Actually red #define LED_BLUE PIN_LED1 -#define LED_STATE_ON 1 // State when LED is litted +#define LED_STATE_ON 1 // State when LED is lit #define BUTTON_PIN (32 + 2) // P1.02 7 diff --git a/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini b/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini index c6cd23314..e7eede80f 100644 --- a/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini +++ b/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini @@ -12,5 +12,3 @@ build_flags = ${nrf52840_base.build_flags} -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/gat562_mesh_trial_tracker> -lib_deps = - ${nrf52840_base.lib_deps} diff --git a/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp b/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp +++ b/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp @@ -36,9 +36,6 @@ void initVariant() 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/nrf52840/gat562_mesh_trial_tracker/variant.h b/variants/nrf52840/gat562_mesh_trial_tracker/variant.h index 6337ac70c..b5632a7fb 100644 --- a/variants/nrf52840/gat562_mesh_trial_tracker/variant.h +++ b/variants/nrf52840/gat562_mesh_trial_tracker/variant.h @@ -46,13 +46,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -63,8 +60,6 @@ extern "C" { #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 @@ -264,8 +259,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #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 diff --git a/variants/nrf52840/heltec_mesh_node_t114-inkhud/platformio.ini b/variants/nrf52840/heltec_mesh_node_t114-inkhud/platformio.ini index 2641a507d..fbdc999a3 100644 --- a/variants/nrf52840/heltec_mesh_node_t114-inkhud/platformio.ini +++ b/variants/nrf52840/heltec_mesh_node_t114-inkhud/platformio.ini @@ -14,7 +14,6 @@ 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/nrf52840/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py ; Add to PIO's Project Tasks pane: preset builds for common displays diff --git a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp index 85c9f4a72..b8b0e21c5 100644 --- a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp +++ b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp @@ -19,6 +19,7 @@ */ #include "variant.h" +#include "Arduino.h" #include "nrf.h" #include "wiring_constants.h" #include "wiring_digital.h" @@ -36,3 +37,10 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); } + +void variant_shutdown() +{ + nrf_gpio_cfg_default(PIN_GPS_PPS); + detachInterrupt(PIN_GPS_PPS); + detachInterrupt(PIN_BUTTON1); +} \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h index 143d20459..2802e4c1d 100644 --- a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h @@ -30,7 +30,6 @@ extern "C" { #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 @@ -124,9 +123,6 @@ No longer populated on PCB #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN -// PCF8563 RTC Module -#define PCF8563_RTC 0x51 - /* * SPI Interfaces */ @@ -141,8 +137,6 @@ No longer populated on PCB #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 @@ -163,7 +157,6 @@ No longer populated on PCB #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (4.90F) -#define HAS_RTC 0 #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/heltec_mesh_node_t114/platformio.ini b/variants/nrf52840/heltec_mesh_node_t114/platformio.ini index c49dadd56..a39872205 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/platformio.ini +++ b/variants/nrf52840/heltec_mesh_node_t114/platformio.ini @@ -1,5 +1,14 @@ ; First prototype nrf52840/sx1262 device [env:heltec-mesh-node-t114] +custom_meshtastic_hw_model = 69 +custom_meshtastic_hw_model_slug = HELTEC_MESH_NODE_T114 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Mesh Node T114 +custom_meshtastic_images = heltec-mesh-node-t114.svg, heltec-mesh-node-t114-case.svg +custom_meshtastic_tags = Heltec + extends = nrf52840_base board = heltec_mesh_node_t114 board_level = pr @@ -13,5 +22,5 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_node_t114> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 - https://github.com/meshtastic/st7789/archive/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip \ No newline at end of file + # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main + https://github.com/meshtastic/st7789/archive/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.cpp b/variants/nrf52840/heltec_mesh_node_t114/variant.cpp index 85c9f4a72..b8b0e21c5 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/variant.cpp +++ b/variants/nrf52840/heltec_mesh_node_t114/variant.cpp @@ -19,6 +19,7 @@ */ #include "variant.h" +#include "Arduino.h" #include "nrf.h" #include "wiring_constants.h" #include "wiring_digital.h" @@ -36,3 +37,10 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); } + +void variant_shutdown() +{ + nrf_gpio_cfg_default(PIN_GPS_PPS); + detachInterrupt(PIN_GPS_PPS); + detachInterrupt(PIN_BUTTON1); +} \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.h b/variants/nrf52840/heltec_mesh_node_t114/variant.h index 28404fcce..e7385c4bb 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t114/variant.h @@ -74,7 +74,6 @@ extern "C" { #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 @@ -150,6 +149,14 @@ No longer populated on PCB #define PIN_SPI1_MOSI ST7789_SDA #define PIN_SPI1_SCK ST7789_SCK +/* + * Bluetooth + */ + +// The bluetooth transmit power on the nRF52840 is adjustable from -20dB to +8dB in steps of 4dB +// so NRF52_BLE_TX_POWER can be set to -20, -16, -12, -8, -4, 0 (default), 4, and 8. +// #define NRF52_BLE_TX_POWER 8 + /* * GPS pins */ @@ -175,9 +182,6 @@ No longer populated on PCB #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN -// PCF8563 RTC Module -#define PCF8563_RTC 0x51 - /* * SPI Interfaces */ @@ -188,8 +192,6 @@ No longer populated on PCB #define PIN_SPI_MOSI (0 + 22) #define PIN_SPI_SCK (0 + 19) -// #define PIN_PWR_EN (0 + 6) - // To debug via the segger JLINK console rather than the CDC-ACM serial device // #define USE_SEGGER @@ -221,7 +223,6 @@ No longer populated on PCB // VBAT=4.04V #define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_2_8 -#define HAS_RTC 0 #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h b/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h index f8202debb..10f628d56 100644 --- a/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h +++ b/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h @@ -50,9 +50,9 @@ void setupNicheGraphics() inkhud->setDisplayResilience(10, 1.5); // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1253; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1253; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1253; // Customize default settings inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? diff --git a/variants/nrf52840/heltec_mesh_pocket/platformio.ini b/variants/nrf52840/heltec_mesh_pocket/platformio.ini index 2fb852226..646304a5a 100644 --- a/variants/nrf52840/heltec_mesh_pocket/platformio.ini +++ b/variants/nrf52840/heltec_mesh_pocket/platformio.ini @@ -1,8 +1,20 @@ ; First prototype nrf52840/sx1262 device [env:heltec-mesh-pocket-5000] +custom_meshtastic_support_level = 1 +custom_meshtastic_images = heltec_mesh_pocket.svg +custom_meshtastic_tags = Heltec + extends = nrf52840_base board = heltec_mesh_pocket debug_tool = jlink +custom_device_hw_model = 94 +custom_meshtastic_hw_model = 94 +custom_meshtastic_hw_model_slug = HELTEC_MESH_POCKET +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_display_name = Heltec Mesh Pocket +custom_meshtastic_actively_supported = true +custom_meshtastic_variant = 5000mAh +custom_meshtastic_key = heltec_mesh_pocket # add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. build_flags = ${nrf52840_base.build_flags} @@ -25,13 +37,19 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_pocket> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 - https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d - + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:heltec-mesh-pocket-5000-inkhud] extends = nrf52840_base, inkhud board = heltec_mesh_pocket +custom_meshtastic_hw_model = 94 +custom_meshtastic_hw_model_slug = HELTEC_MESH_POCKET +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_display_name = Heltec Mesh Pocket +custom_meshtastic_actively_supported = true +custom_meshtastic_variant = 5000mAh InkHUD +custom_meshtastic_key = heltec_mesh_pocket build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_pocket> ${inkhud.build_src_filter} build_flags = ${inkhud.build_flags} @@ -46,9 +64,20 @@ lib_deps = ; First prototype nrf52840/sx1262 device [env:heltec-mesh-pocket-10000] +custom_meshtastic_support_level = 1 +custom_meshtastic_images = heltec_mesh_pocket.svg +custom_meshtastic_tags = Heltec + extends = nrf52840_base board = heltec_mesh_pocket debug_tool = jlink +custom_meshtastic_hw_model = 94 +custom_meshtastic_hw_model_slug = HELTEC_MESH_POCKET +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_display_name = Heltec Mesh Pocket +custom_meshtastic_actively_supported = true +custom_meshtastic_variant = 10000mAh +custom_meshtastic_key = heltec_mesh_pocket # add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. build_flags = ${nrf52840_base.build_flags} @@ -71,13 +100,19 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_pocket> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 - https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d - + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:heltec-mesh-pocket-10000-inkhud] extends = nrf52840_base, inkhud board = heltec_mesh_pocket +custom_meshtastic_hw_model = 94 +custom_meshtastic_hw_model_slug = HELTEC_MESH_POCKET +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_display_name = Heltec Mesh Pocket +custom_meshtastic_actively_supported = true +custom_meshtastic_variant = 10000mAh InkHUD +custom_meshtastic_key = heltec_mesh_pocket build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_pocket> ${inkhud.build_src_filter} build_flags = ${inkhud.build_flags} diff --git a/variants/nrf52840/heltec_mesh_pocket/variant.h b/variants/nrf52840/heltec_mesh_pocket/variant.h index f4f695b34..a86faf575 100644 --- a/variants/nrf52840/heltec_mesh_pocket/variant.h +++ b/variants/nrf52840/heltec_mesh_pocket/variant.h @@ -26,8 +26,6 @@ extern "C" { #define LED_RED PIN_LED1 #define LED_BLUE PIN_LED1 #define LED_GREEN PIN_LED1 -#define LED_BUILTIN LED_BLUE -#define LED_CONN LED_BLUE #define LED_STATE_ON 0 // State when LED is lit /* @@ -100,8 +98,6 @@ No longer populated on PCB #define PIN_SPI_MOSI (0 + 5) #define PIN_SPI_SCK (0 + 4) -// #define PIN_PWR_EN (0 + 6) - // To debug via the segger JLINK console rather than the CDC-ACM serial device // #define USE_SEGGER @@ -130,7 +126,6 @@ No longer populated on PCB #undef HAS_GPS #define HAS_GPS 0 -#define HAS_RTC 0 #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/heltec_mesh_solar/platformio.ini b/variants/nrf52840/heltec_mesh_solar/platformio.ini index 36a7904d6..69264f0df 100644 --- a/variants/nrf52840/heltec_mesh_solar/platformio.ini +++ b/variants/nrf52840/heltec_mesh_solar/platformio.ini @@ -13,10 +13,21 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_solar> lib_deps = ${nrf52840_base.lib_deps} + # renovate: datasource=git-refs depName=NMIoT-meshsolar packageName=https://github.com/NMIoT/meshsolar gitBranch=main https://github.com/NMIoT/meshsolar/archive/dfc5330dad443982e6cdd37a61d33fc7252f468b.zip - lewisxhe/PCF8563_Library@^1.0.1 - ArduinoJson@6.21.4 + # renovate: datasource=custom.pio depName=ArduinoJson packageName=bblanchon/library/ArduinoJson + bblanchon/ArduinoJson@6.21.5 + [env:heltec-mesh-solar] +custom_meshtastic_hw_model = 108 +custom_meshtastic_hw_model_slug = HELTEC_MESH_SOLAR +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = false +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec MeshSolar +custom_meshtastic_images = heltec-mesh-solar.svg +custom_meshtastic_tags = Heltec + extends = heltec_mesh_solar_base build_flags = ${heltec_mesh_solar_base.build_flags} -DSPI_INTERFACES_COUNT=1 @@ -46,8 +57,17 @@ build_flags = ${heltec_mesh_solar_base.build_flags} -DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. -DEINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" + -DENABLE_MESSAGE_PERSISTENCE=0 ; Disable flash persistence for space-limited build + -DMESHTASTIC_EXCLUDE_WIFI=1 + -DMESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 + -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 + -DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1 + -DMESHTASTIC_EXCLUDE_STOREFORWARD=1 + -DMESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 + -DMESHTASTIC_EXCLUDE_WAYPOINT=1 lib_deps = ${heltec_mesh_solar_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:heltec-mesh-solar-inkhud] @@ -71,7 +91,6 @@ lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${heltec_mesh_solar_base.lib_deps} - [env:heltec-mesh-solar-oled] extends = heltec_mesh_solar_base build_flags = ${heltec_mesh_solar_base.build_flags} @@ -112,4 +131,5 @@ build_flags = ${heltec_mesh_solar_base.build_flags} -DPIN_SPI1_SCK=ST7789_SCK lib_deps = ${heltec_mesh_solar_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main https://github.com/meshtastic/st7789/archive/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip diff --git a/variants/nrf52840/heltec_mesh_solar/variant.cpp b/variants/nrf52840/heltec_mesh_solar/variant.cpp index c13f006d7..3b2612da8 100644 --- a/variants/nrf52840/heltec_mesh_solar/variant.cpp +++ b/variants/nrf52840/heltec_mesh_solar/variant.cpp @@ -19,6 +19,7 @@ */ #include "variant.h" +#include "Arduino.h" #include "nrf.h" #include "wiring_constants.h" #include "wiring_digital.h" @@ -38,3 +39,10 @@ void initVariant() digitalWrite(PIN_SCREEN_VDD_CTL, LOW); // Start with power on #endif } + +void variant_shutdown() +{ + nrf_gpio_cfg_default(PIN_GPS_PPS); + detachInterrupt(PIN_GPS_PPS); + detachInterrupt(PIN_BUTTON1); +} \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_solar/variant.h b/variants/nrf52840/heltec_mesh_solar/variant.h index 7a8fc579f..8d4db4bea 100644 --- a/variants/nrf52840/heltec_mesh_solar/variant.h +++ b/variants/nrf52840/heltec_mesh_solar/variant.h @@ -39,16 +39,15 @@ extern "C" { #define NUM_ANALOG_INPUTS (1) #define NUM_ANALOG_OUTPUTS (0) -#define PIN_LED1 (0 + 4) // green (confirmed on 1.0 board) -#define LED_BLUE PIN_LED1 // fake for bluefruit library +#define PIN_LED1 (32 + 15) // 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 1 // How many neopixels are connected -#define NEOPIXEL_DATA (32 + 15) // gpio pin used to send data to the neopixels -#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use +// #define HAS_NEOPIXEL // Enable the use of neopixels +// #define NEOPIXEL_COUNT 1 // How many neopixels are connected +// #define NEOPIXEL_DATA (32 + 15) // gpio pin used to send data to the neopixels +// #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use /* * Buttons @@ -60,8 +59,8 @@ extern "C" { /* No longer populated on PCB */ -#define PIN_SERIAL2_RX (0 + 9) -#define PIN_SERIAL2_TX (0 + 10) +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) /* * I2C @@ -133,16 +132,21 @@ No longer populated on PCB #define PIN_SPI_MOSI (0 + 22) #define PIN_SPI_SCK (0 + 19) -// #define PIN_PWR_EN (0 + 6) - // To debug via the segger JLINK console rather than the CDC-ACM serial device // #define USE_SEGGER +// Hardware watchdog +#define HAS_HARDWARE_WATCHDOG +#define HARDWARE_WATCHDOG_DONE (0 + 9) +#define HARDWARE_WATCHDOG_WAKE (0 + 10) +#define HARDWARE_WATCHDOG_TIMEOUT_MS (6 * 60 * 1000) // 6 minute watchdog + #define BQ4050_SDA_PIN (32 + 1) // I2C data line pin #define BQ4050_SCL_PIN (32 + 0) // I2C clock line pin #define BQ4050_EMERGENCY_SHUTDOWN_PIN (32 + 3) // Emergency shutdown pin -#define HAS_RTC 0 +#define SERIAL_PRINT_PORT 0 + #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/meshlink/platformio.ini b/variants/nrf52840/meshlink/platformio.ini index e0f4a2b9b..e2631affe 100644 --- a/variants/nrf52840/meshlink/platformio.ini +++ b/variants/nrf52840/meshlink/platformio.ini @@ -14,8 +14,6 @@ build_flags = ${nrf52840_base.build_flags} -DRADIOLIB_EXCLUDE_LR11X0=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshlink> -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) ; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds @@ -48,7 +46,8 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshlink> lib_deps = ${nrf52840_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip debug_tool = jlink ; 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 diff --git a/variants/nrf52840/meshlink/variant.cpp b/variants/nrf52840/meshlink/variant.cpp index 81a5097c4..f35bbd1af 100644 --- a/variants/nrf52840/meshlink/variant.cpp +++ b/variants/nrf52840/meshlink/variant.cpp @@ -20,4 +20,11 @@ void initVariant() pinMode(PIN_WD_EN, OUTPUT); digitalWrite(PIN_WD_EN, HIGH); // Enable the Watchdog at boot #endif +} + +void variant_shutdown() +{ +#ifdef PIN_WD_EN + digitalWrite(PIN_WD_EN, LOW); +#endif } \ No newline at end of file diff --git a/variants/nrf52840/meshlink/variant.h b/variants/nrf52840/meshlink/variant.h index d1dba574f..00107ac34 100644 --- a/variants/nrf52840/meshlink/variant.h +++ b/variants/nrf52840/meshlink/variant.h @@ -31,10 +31,8 @@ extern "C" { // LEDs #define PIN_LED1 (24) // Built in white led for status #define LED_BLUE PIN_LED1 -#define LED_BUILTIN PIN_LED1 -#define LED_STATE_ON 0 // State when LED is litted -#define LED_INVERTED 1 +#define LED_STATE_ON 0 // State when LED is lit // Testing USB detection // #define NRF_APM @@ -54,6 +52,7 @@ extern "C" { */ #define PIN_SERIAL1_RX (32 + 8) #define PIN_SERIAL1_TX (7) +#define SERIAL_PRINT_PORT 0 /* * SPI Interfaces @@ -150,4 +149,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/nrf52840/meshtiny/platformio.ini b/variants/nrf52840/meshtiny/platformio.ini index 5f03f5cb2..7e1e2e510 100644 --- a/variants/nrf52840/meshtiny/platformio.ini +++ b/variants/nrf52840/meshtiny/platformio.ini @@ -7,5 +7,3 @@ build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/meshtiny -D MESHT -D USE_PIN_BUZZER -D MESHTASTIC_EXCLUDE_GPS=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshtiny> -lib_deps = - ${nrf52840_base.lib_deps} diff --git a/variants/nrf52840/meshtiny/variant.cpp b/variants/nrf52840/meshtiny/variant.cpp index 2e8b00e4b..aae3da9f2 100644 --- a/variants/nrf52840/meshtiny/variant.cpp +++ b/variants/nrf52840/meshtiny/variant.cpp @@ -32,12 +32,12 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - // LED1 & LED2 + // LED1 & LED_BLUE pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(LED_BLUE, OUTPUT); + ledOff(LED_BLUE); // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); diff --git a/variants/nrf52840/meshtiny/variant.h b/variants/nrf52840/meshtiny/variant.h index 8d634ba60..4289fef4c 100644 --- a/variants/nrf52840/meshtiny/variant.h +++ b/variants/nrf52840/meshtiny/variant.h @@ -47,13 +47,9 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 #define LED_STATE_ON 1 // State when LED is litted @@ -66,8 +62,6 @@ extern "C" { #define INPUTDRIVER_ENCODER_BTN 28 #define UPDOWN_LONG_PRESS_REPEAT_INTERVAL 150 -#define CANNED_MESSAGE_MODULE_ENABLE 1 - /* * Buzzer - PWM */ diff --git a/variants/nrf52840/monteops_hw1/platformio.ini b/variants/nrf52840/monteops_hw1/platformio.ini index 5426aee7f..e783e130e 100644 --- a/variants/nrf52840/monteops_hw1/platformio.ini +++ b/variants/nrf52840/monteops_hw1/platformio.ini @@ -10,6 +10,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/monteop lib_deps = ${nrf52840_base.lib_deps} ${networking_base.lib_deps} + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) diff --git a/variants/nrf52840/monteops_hw1/variant.cpp b/variants/nrf52840/monteops_hw1/variant.cpp index 75cca1dc3..81ae9f482 100644 --- a/variants/nrf52840/monteops_hw1/variant.cpp +++ b/variants/nrf52840/monteops_hw1/variant.cpp @@ -35,7 +35,4 @@ void initVariant() // LED1 & LED2 pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); } diff --git a/variants/nrf52840/monteops_hw1/variant.h b/variants/nrf52840/monteops_hw1/variant.h index 97536b169..a7fa7125b 100644 --- a/variants/nrf52840/monteops_hw1/variant.h +++ b/variants/nrf52840/monteops_hw1/variant.h @@ -50,13 +50,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) // Connected to WWAN host LED (if present) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) // Connected to WWAN host LED (if present) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -67,8 +64,6 @@ extern "C" { // #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 @@ -213,8 +208,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (1.73F) -// #define HAS_RTC 1 - #define HAS_ETHERNET 1 #define PIN_ETHERNET_RESET 21 diff --git a/variants/nrf52840/muzi_base/platformio.ini b/variants/nrf52840/muzi_base/platformio.ini index 49393f4e0..52c558ff1 100644 --- a/variants/nrf52840/muzi_base/platformio.ini +++ b/variants/nrf52840/muzi_base/platformio.ini @@ -1,4 +1,13 @@ [env:muzi-base] +custom_meshtastic_hw_model = 93 +custom_meshtastic_hw_model_slug = MUZI_BASE +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = muzi BASE +custom_meshtastic_images = muzi_base.svg +custom_meshtastic_tags = muzi + extends = nrf52840_base board = muzi-base build_flags = ${nrf52840_base.build_flags} @@ -10,6 +19,5 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52840_base.build_src_filter} +<../variants/nrf52840/muzi_base> lib_deps = ${nrf52840_base.lib_deps} + # renovate: datasource=custom.pio depName=ArtronShop_RX8130CE packageName=artronshop/library/ArtronShop_RX8130CE artronshop/ArtronShop_RX8130CE@1.0.0 - - diff --git a/variants/nrf52840/muzi_base/variant.cpp b/variants/nrf52840/muzi_base/variant.cpp index da01de974..e6178a968 100644 --- a/variants/nrf52840/muzi_base/variant.cpp +++ b/variants/nrf52840/muzi_base/variant.cpp @@ -63,8 +63,8 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); digitalWrite(PIN_LED1, HIGH); - pinMode(PIN_LED2, OUTPUT); - digitalWrite(PIN_LED2, HIGH); + pinMode(LED_BLUE, OUTPUT); + digitalWrite(LED_BLUE, HIGH); // Initialize LoRa pins pinMode(SX126X_RESET, OUTPUT); diff --git a/variants/nrf52840/muzi_base/variant.h b/variants/nrf52840/muzi_base/variant.h index 96604c400..0cbd0e3ef 100644 --- a/variants/nrf52840/muzi_base/variant.h +++ b/variants/nrf52840/muzi_base/variant.h @@ -38,15 +38,13 @@ extern "C" { #define COMPASS_ORIENTATION meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270 #define HAS_ICM20948 // forces the i2c address to be seen as this sensor -#define HAS_RTC 1 #define RX8130CE_RTC 0x32 // LEDs #define PIN_LED1 (32 + 3) // P1.03, Green -#define PIN_LED2 (32 + 4) // P1.04, Blue +#define LED_BLUE (32 + 4) // P1.04, Blue -#define LED_BUILTIN -1 // PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 0 // State when LED is lit // Buttons @@ -176,6 +174,8 @@ extern "C" { #define EXTERNAL_FLASH_DEVICES W25Q32JVSS #define EXTERNAL_FLASH_USE_QSPI +#define SERIAL_PRINT_PORT 0 + // NFC is disabled via CONFIG_NFCT_PINS_AS_GPIOS=1 build flag // This configures P0.09 and P0.10 as regular GPIO pins instead of NFC pins diff --git a/variants/nrf52840/nano-g2-ultra/platformio.ini b/variants/nrf52840/nano-g2-ultra/platformio.ini index f697a90dd..7e593a49a 100644 --- a/variants/nrf52840/nano-g2-ultra/platformio.ini +++ b/variants/nrf52840/nano-g2-ultra/platformio.ini @@ -1,5 +1,14 @@ ; First prototype eink/nrf52840/sx1262 device [env:nano-g2-ultra] +custom_meshtastic_hw_model = 18 +custom_meshtastic_hw_model_slug = NANO_G2_ULTRA +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 2 +custom_meshtastic_display_name = Nano G2 Ultra +custom_meshtastic_images = nano-g2-ultra.svg +custom_meshtastic_tags = B&Q + extends = nrf52840_base board = nano-g2-ultra debug_tool = jlink @@ -10,5 +19,6 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/nano-g2-ultra> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 ;upload_protocol = fs diff --git a/variants/nrf52840/nano-g2-ultra/variant.h b/variants/nrf52840/nano-g2-ultra/variant.h index fd837f66e..631af72d8 100644 --- a/variants/nrf52840/nano-g2-ultra/variant.h +++ b/variants/nrf52840/nano-g2-ultra/variant.h @@ -41,18 +41,6 @@ extern "C" { #define NUM_ANALOG_INPUTS (1) #define NUM_ANALOG_OUTPUTS (0) -// LEDs -#define PIN_LED1 (-1) -#define PIN_LED2 (-1) -#define PIN_LED3 (-1) - -#define LED_RED PIN_LED3 -#define LED_BLUE PIN_LED1 -#define LED_GREEN PIN_LED2 - -#define LED_BUILTIN LED_BLUE -#define LED_CONN PIN_GREEN - #define LED_STATE_ON 0 // State when LED is lit /* @@ -87,8 +75,6 @@ static const uint8_t A4 = PIN_A4; #define PIN_WIRE_SDA (0 + 17) #define PIN_WIRE_SCL (0 + 15) -#define PIN_RTC_INT (0 + 14) // Interrupt from the PCF8563 RTC - /* External serial flash W25Q16JV_IQ */ @@ -132,15 +118,16 @@ External serial flash W25Q16JV_IQ #define GPS_L76K #define PIN_GPS_STANDBY (0 + 13) // An output to wake GPS, low means allow sleep, high means force wake STANDBY -#define PIN_GPS_TX (0 + 10) // This is for bits going TOWARDS the CPU -#define PIN_GPS_RX (0 + 9) // This is for bits going TOWARDS the GPS +#define GPS_TX_PIN (0 + 10) // This is for bits going FROM the CPU +#define GPS_RX_PIN (0 + 9) // This is for bits going FROM the GPS // #define GPS_THREAD_INTERVAL 50 -#define PIN_SERIAL1_TX PIN_GPS_TX -#define PIN_SERIAL1_RX PIN_GPS_RX +#define PIN_SERIAL1_TX GPS_TX_PIN +#define PIN_SERIAL1_RX GPS_RX_PIN // PCF8563 RTC Module +#define PIN_RTC_INT (0 + 14) // Interrupt from the PCF8563 RTC #define PCF8563_RTC 0x51 /* @@ -153,8 +140,6 @@ External serial flash W25Q16JV_IQ #define PIN_SPI_MOSI (0 + 11) #define PIN_SPI_SCK (0 + 12) -// #define PIN_PWR_EN (0 + 6) - // To debug via the segger JLINK console rather than the CDC-ACM serial device // #define USE_SEGGER @@ -169,8 +154,6 @@ External serial flash W25Q16JV_IQ #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (2.0F) -#define HAS_RTC 1 - /** OLED Screen Model */ diff --git a/variants/nrf52840/nrf52.ini b/variants/nrf52840/nrf52.ini index 5da1aebb5..727d2c741 100644 --- a/variants/nrf52840/nrf52.ini +++ b/variants/nrf52840/nrf52.ini @@ -2,12 +2,12 @@ ; Instead of the standard nordicnrf52 platform, we use our fork which has our added variant files platform = # renovate: datasource=custom.pio depName=platformio/nordicnrf52 packageName=platformio/platform/nordicnrf52 - platformio/nordicnrf52@^10.8.0 + platformio/nordicnrf52@10.10.0 extends = arduino_base platform_packages = ; our custom Git version until they merge our PR # TODO renovate - platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino#c770c8a16a351b55b86e347a3d9d7b74ad0bbf39 + platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino#74096746e5f167a2ff22e483d8e79bb1aef00591 ; Don't renovate toolchain-gccarmnoneeabi platformio/toolchain-gccarmnoneeabi@~1.90301.0 @@ -19,7 +19,6 @@ build_type = release build_flags = -include variants/nrf52840/cpp_overrides/lfs_util.h ${arduino_base.build_flags} - -DSERIAL_BUFFER_SIZE=1024 -Wno-unused-variable -Isrc/platform/nrf52 -DLFS_NO_ASSERT ; Disable LFS assertions , see https://github.com/meshtastic/firmware/pull/3818 diff --git a/variants/nrf52840/nrf52832.ini b/variants/nrf52840/nrf52832.ini index ce94283b1..b106fe7d4 100644 --- a/variants/nrf52840/nrf52832.ini +++ b/variants/nrf52840/nrf52832.ini @@ -1,7 +1,6 @@ [nrf52832_base] extends = nrf52_base -build_flags = ${nrf52_base.build_flags} - -lib_deps = - ${nrf52_base.lib_deps} +build_flags = + ${nrf52_base.build_flags} + -DSERIAL_BUFFER_SIZE=1024 diff --git a/variants/nrf52840/nrf52840.ini b/variants/nrf52840/nrf52840.ini index e13443152..09b2ef97d 100644 --- a/variants/nrf52840/nrf52840.ini +++ b/variants/nrf52840/nrf52840.ini @@ -1,7 +1,9 @@ [nrf52840_base] extends = nrf52_base -build_flags = ${nrf52_base.build_flags} +build_flags = + ${nrf52_base.build_flags} + -DSERIAL_BUFFER_SIZE=4096 lib_deps = ${nrf52_base.lib_deps} diff --git a/variants/nrf52840/r1-neo/platformio.ini b/variants/nrf52840/r1-neo/platformio.ini index 60f1f6ae1..85fe49cf1 100644 --- a/variants/nrf52840/r1-neo/platformio.ini +++ b/variants/nrf52840/r1-neo/platformio.ini @@ -1,5 +1,14 @@ ; The R1 Neo board [env:r1-neo] +custom_meshtastic_hw_model = 101 +custom_meshtastic_hw_model_slug = MUZI_R1_NEO +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = muzi R1 Neo +custom_meshtastic_images = muzi_r1_neo.svg +custom_meshtastic_tags = muzi + extends = nrf52840_base board = r1-neo board_check = true @@ -13,6 +22,9 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/r1-neo> lib_deps = ${nrf52840_base.lib_deps} ${networking_base.lib_deps} + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip - rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 + # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library + rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 + # renovate: datasource=custom.pio depName=ArtronShop_RX8130CE packageName=artronshop/library/ArtronShop_RX8130CE artronshop/ArtronShop_RX8130CE@1.0.0 diff --git a/variants/nrf52840/r1-neo/variant.cpp b/variants/nrf52840/r1-neo/variant.cpp index f87c041aa..c36e88602 100644 --- a/variants/nrf52840/r1-neo/variant.cpp +++ b/variants/nrf52840/r1-neo/variant.cpp @@ -36,10 +36,15 @@ void initVariant() 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); } + +void earlyInitVariant() +{ + pinMode(DCDC_EN_HOLD, OUTPUT); + digitalWrite(DCDC_EN_HOLD, HIGH); + pinMode(NRF_ON, OUTPUT); + digitalWrite(NRF_ON, HIGH); +} diff --git a/variants/nrf52840/r1-neo/variant.h b/variants/nrf52840/r1-neo/variant.h index b1d96ebd0..42d44d673 100644 --- a/variants/nrf52840/r1-neo/variant.h +++ b/variants/nrf52840/r1-neo/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (32 + 4) // P1.04 Controls Green LED -#define PIN_LED2 (28) // P0.28 Controls Blue LED - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (28) // P0.28 Controls Blue LED #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -135,8 +132,6 @@ static const uint8_t SCK = PIN_SPI_SCK; #define ADC_MULTIPLIER 1.667 #define OCV_ARRAY 4120, 4020, 4000, 3940, 3870, 3820, 3750, 3630, 3550, 3450, 3100 -#define HAS_RTC 1 - #define RX8130CE_RTC 0x32 #ifdef __cplusplus diff --git a/variants/nrf52840/rak2560/platformio.ini b/variants/nrf52840/rak2560/platformio.ini index 021e6d03b..1703a13ae 100644 --- a/variants/nrf52840/rak2560/platformio.ini +++ b/variants/nrf52840/rak2560/platformio.ini @@ -1,5 +1,14 @@ ; Firmware for the WisMesh HUB RAK2560, including a onewire module to talk to the RAK 9154 solar battery. [env:rak2560] +custom_meshtastic_hw_model = 22 +custom_meshtastic_hw_model_slug = WISMESH_HUB +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = RAK WisMesh Repeater +custom_meshtastic_images = rak2560.svg +custom_meshtastic_tags = RAK + extends = nrf52840_base board = wiscore_rak4631 board_check = true @@ -13,8 +22,10 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak2560> + + + lib_deps = ${nrf52840_base.lib_deps} - ${nrf52_networking_base.lib_deps} - melopero/Melopero RV3028@^1.1.0 + ${networking_base.lib_deps} + # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 + melopero/Melopero RV3028@1.2.0 + # renovate: datasource=github-tags depName=RAK-OneWireSerial packageName=beegee-tokyo/RAK-OneWireSerial https://github.com/beegee-tokyo/RAK-OneWireSerial/archive/0.0.2.zip debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) diff --git a/variants/nrf52840/rak2560/variant.cpp b/variants/nrf52840/rak2560/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/rak2560/variant.cpp +++ b/variants/nrf52840/rak2560/variant.cpp @@ -36,9 +36,6 @@ void initVariant() 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/nrf52840/rak2560/variant.h b/variants/nrf52840/rak2560/variant.h index f922e8a61..acd1ae60e 100644 --- a/variants/nrf52840/rak2560/variant.h +++ b/variants/nrf52840/rak2560/variant.h @@ -46,13 +46,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -63,8 +60,6 @@ extern "C" { #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 @@ -249,8 +244,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER 1.73 -#define HAS_RTC 1 - #define RAK_4631 1 #define HALF_UART_PIN PIN_SERIAL1_RX diff --git a/variants/nrf52840/rak3401_1watt/platformio.ini b/variants/nrf52840/rak3401_1watt/platformio.ini index 1a915a6b3..bb8fa28df 100644 --- a/variants/nrf52840/rak3401_1watt/platformio.ini +++ b/variants/nrf52840/rak3401_1watt/platformio.ini @@ -1,5 +1,15 @@ ; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 [env:rak3401-1watt] +custom_meshtastic_hw_model = 117 +custom_meshtastic_hw_model_slug = RAK3401 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = RAK3401 1W +custom_meshtastic_images = rak3401.svg +custom_meshtastic_tags = RAK +custom_meshtastic_requires_dfu = true + extends = nrf52840_base board = wiscore_rak4631 board_check = true @@ -16,9 +26,13 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak3401 lib_deps = ${nrf52840_base.lib_deps} ${networking_base.lib_deps} - melopero/Melopero RV3028@^1.1.0 - rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 - beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 + # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 + melopero/Melopero RV3028@1.2.0 + # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library + rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 + # renovate: datasource=custom.pio depName=RAK12035_SoilMoisture packageName=beegee-tokyo/library/RAK12035_SoilMoisture + beegee-tokyo/RAK12035_SoilMoisture@1.0.4 + # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) diff --git a/variants/nrf52840/rak3401_1watt/variant.cpp b/variants/nrf52840/rak3401_1watt/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/rak3401_1watt/variant.cpp +++ b/variants/nrf52840/rak3401_1watt/variant.cpp @@ -36,9 +36,6 @@ void initVariant() 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/nrf52840/rak3401_1watt/variant.h b/variants/nrf52840/rak3401_1watt/variant.h index d4bb1a175..80b09cf69 100644 --- a/variants/nrf52840/rak3401_1watt/variant.h +++ b/variants/nrf52840/rak3401_1watt/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -211,8 +208,6 @@ static const uint8_t SCK = PIN_SPI_SCK; #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER 1.73 -#define HAS_RTC 1 - #define RAK_4631 1 #ifdef __cplusplus diff --git a/variants/nrf52840/rak4631/platformio.ini b/variants/nrf52840/rak4631/platformio.ini index 0ef661af8..4a96fc8d9 100644 --- a/variants/nrf52840/rak4631/platformio.ini +++ b/variants/nrf52840/rak4631/platformio.ini @@ -1,5 +1,14 @@ ; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 [env:rak4631] +custom_meshtastic_hw_model = 9 +custom_meshtastic_hw_model_slug = RAK4631 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = RAK WisBlock 4631 +custom_meshtastic_images = rak4631.svg, rak4631_case.svg +custom_meshtastic_tags = RAK + extends = nrf52840_base board = wiscore_rak4631 board_level = pr @@ -22,11 +31,15 @@ build_src_filter = ${nrf52_base.build_src_filter} \ - lib_deps = ${nrf52840_base.lib_deps} - ${nrf52_networking_base.lib_deps} - melopero/Melopero RV3028@^1.1.0 + ${networking_base.lib_deps} + # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 + melopero/Melopero RV3028@1.2.0 + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S 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 + # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library + rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 + # renovate: datasource=custom.pio depName=RAK12035_SoilMoisture packageName=beegee-tokyo/library/RAK12035_SoilMoisture + beegee-tokyo/RAK12035_SoilMoisture@1.0.4 # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip @@ -42,7 +55,7 @@ extends = env:rak4631 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 +; platform_packages = platformio/tool-openocd@3.1200.0 build_flags = ${env:rak4631.build_flags} @@ -50,6 +63,7 @@ build_flags = lib_deps = ${env:rak4631.lib_deps} + # TODO renovate 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. @@ -59,6 +73,6 @@ lib_deps = ; 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 +; eventually use platformio/tool-pyocd@2.3600.0 instad ;upload_protocol = custom ;upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE diff --git a/variants/nrf52840/rak4631/variant.cpp b/variants/nrf52840/rak4631/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/rak4631/variant.cpp +++ b/variants/nrf52840/rak4631/variant.cpp @@ -36,9 +36,6 @@ void initVariant() 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/nrf52840/rak4631/variant.h b/variants/nrf52840/rak4631/variant.h index 302e531d5..6a6b32f27 100644 --- a/variants/nrf52840/rak4631/variant.h +++ b/variants/nrf52840/rak4631/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -62,8 +59,6 @@ extern "C" { #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 @@ -253,9 +248,13 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define RV3028_RTC (uint8_t)0b1010010 // RAK18001 Buzzer in Slot C -// #define PIN_BUZZER 21 // IO3 is PWM2 +#define PIN_BUZZER 21 // IO3 is PWM2 // NEW: set this via protobuf instead! +// RAK4631 custom ringtone +#undef USERPREFS_RINGTONE_RTTTL +#define USERPREFS_RINGTONE_RTTTL "Rak:d=32,o=5,b=200:b7,p,b7,4p,p" + // Battery // The battery sense is hooked to pin A0 (5) #define BATTERY_PIN PIN_A0 @@ -281,8 +280,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // VDD=3.3V AIN3=6/8*VDD=2.47V VBAT=1.66*AIN3=4.1V #define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_11_16 -#define HAS_RTC 1 - #define HAS_ETHERNET 1 #define RAK_4631 1 diff --git a/variants/nrf52840/rak4631_epaper/platformio.ini b/variants/nrf52840/rak4631_epaper/platformio.ini index 704520f8d..f0da832cb 100644 --- a/variants/nrf52840/rak4631_epaper/platformio.ini +++ b/variants/nrf52840/rak4631_epaper/platformio.ini @@ -14,11 +14,16 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631_epaper> lib_deps = ${nrf52840_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 - 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 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.6 + # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 + melopero/Melopero RV3028@1.2.0 + # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library + rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 + # renovate: datasource=custom.pio depName=RAK12034 packageName=beegee-tokyo/library/RAKwireless RAK12034 + beegee-tokyo/RAKwireless RAK12034@1.0.0 + # renovate: datasource=custom.pio depName=RAK12035_SoilMoisture packageName=beegee-tokyo/library/RAK12035_SoilMoisture + 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/nrf52840/rak4631_epaper/variant.cpp b/variants/nrf52840/rak4631_epaper/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/rak4631_epaper/variant.cpp +++ b/variants/nrf52840/rak4631_epaper/variant.cpp @@ -36,9 +36,6 @@ void initVariant() 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/nrf52840/rak4631_epaper/variant.h b/variants/nrf52840/rak4631_epaper/variant.h index c1e11bee5..82c26af7b 100644 --- a/variants/nrf52840/rak4631_epaper/variant.h +++ b/variants/nrf52840/rak4631_epaper/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -62,8 +59,6 @@ extern "C" { #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 @@ -222,8 +217,6 @@ static const uint8_t SCK = PIN_SPI_SCK; #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER 1.73 -#define HAS_RTC 1 - #define RAK_4631 1 #ifdef __cplusplus diff --git a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini index e0156668b..112ddfc29 100644 --- a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini +++ b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini @@ -16,11 +16,16 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631_epaper_onrxtx> lib_deps = ${nrf52840_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 - 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 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.6 + # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 + melopero/Melopero RV3028@1.2.0 + # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library + rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 + # renovate: datasource=custom.pio depName=RAK12034 packageName=beegee-tokyo/library/RAKwireless RAK12034 + beegee-tokyo/RAKwireless RAK12034@1.0.0 + # renovate: datasource=custom.pio depName=RAK12035_SoilMoisture packageName=beegee-tokyo/library/RAK12035_SoilMoisture + 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/nrf52840/rak4631_epaper_onrxtx/variant.cpp b/variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp +++ b/variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp @@ -36,9 +36,6 @@ void initVariant() 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/nrf52840/rak4631_epaper_onrxtx/variant.h b/variants/nrf52840/rak4631_epaper_onrxtx/variant.h index 1f8257e8e..2d34ab84c 100644 --- a/variants/nrf52840/rak4631_epaper_onrxtx/variant.h +++ b/variants/nrf52840/rak4631_epaper_onrxtx/variant.h @@ -27,13 +27,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -195,8 +192,6 @@ static const uint8_t SCK = PIN_SPI_SCK; // #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 // #define ADC_MULTIPLIER 1.73 -// #define HAS_RTC 1 - #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/rak4631_eth_gw/platformio.ini b/variants/nrf52840/rak4631_eth_gw/platformio.ini index 3c61e3498..e06a271aa 100644 --- a/variants/nrf52840/rak4631_eth_gw/platformio.ini +++ b/variants/nrf52840/rak4631_eth_gw/platformio.ini @@ -27,12 +27,16 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631 lib_deps = ${nrf52840_base.lib_deps} ${networking_base.lib_deps} - melopero/Melopero RV3028@^1.1.0 + # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 + melopero/Melopero RV3028@1.2.0 + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip - rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 + # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library + rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip - bblanchon/ArduinoJson @ 6.21.4 + # renovate: datasource=custom.pio depName=ArduinoJson packageName=bblanchon/library/ArduinoJson + bblanchon/ArduinoJson@6.21.5 ; 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 @@ -45,7 +49,7 @@ extends = env:rak4631 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 +; platform_packages = platformio/tool-openocd@3.1200.0 build_flags = ${env:rak4631_eth_gw.build_flags} @@ -53,6 +57,7 @@ build_flags = lib_deps = ${env:rak4631_eth_gw.lib_deps} + # TODO renovate 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. @@ -62,6 +67,6 @@ lib_deps = ; 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 +; eventually use platformio/tool-pyocd@2.3600.0 instad ;upload_protocol = custom ;upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE diff --git a/variants/nrf52840/rak4631_eth_gw/variant.cpp b/variants/nrf52840/rak4631_eth_gw/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/rak4631_eth_gw/variant.cpp +++ b/variants/nrf52840/rak4631_eth_gw/variant.cpp @@ -36,9 +36,6 @@ void initVariant() 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/nrf52840/rak4631_eth_gw/variant.h b/variants/nrf52840/rak4631_eth_gw/variant.h index c8a2f83ae..eb1d558ea 100644 --- a/variants/nrf52840/rak4631_eth_gw/variant.h +++ b/variants/nrf52840/rak4631_eth_gw/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -62,8 +59,6 @@ extern "C" { #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 @@ -254,8 +249,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #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 diff --git a/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini b/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini index d7dab2678..07d763df3 100644 --- a/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini +++ b/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini @@ -1,5 +1,14 @@ ; NomadStar Meteor Pro based on RAK4631 with RGBW LED LP5562 support [env:rak4631_nomadstar_meteor_pro] +custom_meshtastic_hw_model = 96 +custom_meshtastic_hw_model_slug = NOMADSTAR_METEOR_PRO +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = NomadStar Meteor Pro +custom_meshtastic_images = meteor_pro.svg +custom_meshtastic_tags = NomadStar + extends = nrf52840_base board = wiscore_rak4631 board_check = true @@ -15,6 +24,7 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631_nomadstar_meteor_pro> + + lib_deps = ${nrf52840_base.lib_deps} + # TODO renovate 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) @@ -29,7 +39,7 @@ 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 +; platform_packages = platformio/tool-openocd@3.1200.0 build_flags = ${env:rak4631.build_flags} @@ -37,6 +47,7 @@ build_flags = lib_deps = ${env:rak4631.lib_deps} + # TODO renovate 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. @@ -46,6 +57,6 @@ lib_deps = ; 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 +; 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/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp +++ b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp @@ -36,9 +36,6 @@ void initVariant() 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/nrf52840/rak4631_nomadstar_meteor_pro/variant.h b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h index 51baf3ada..aea497305 100644 --- a/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h +++ b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -62,8 +59,6 @@ extern "C" { #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 @@ -249,8 +244,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #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 diff --git a/variants/nrf52840/rak_wismeshtag/platformio.ini b/variants/nrf52840/rak_wismeshtag/platformio.ini index f04d1f186..1e6e63e60 100644 --- a/variants/nrf52840/rak_wismeshtag/platformio.ini +++ b/variants/nrf52840/rak_wismeshtag/platformio.ini @@ -1,8 +1,17 @@ ; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 [env:rak_wismeshtag] +custom_meshtastic_support_level = 1 +custom_meshtastic_images = rak_wismesh_tag.svg +custom_meshtastic_tags = RAK + extends = nrf52840_base board = wiscore_rak4631 board_check = true +custom_meshtastic_hw_model = 105 +custom_meshtastic_hw_model_slug = WISMESH_TAG +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_display_name = RAK WisMesh Tag +custom_meshtastic_actively_supported = true build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/rak_wismeshtag -D WISMESH_TAG @@ -12,5 +21,3 @@ build_flags = ${nrf52840_base.build_flags} -DRADIOLIB_EXCLUDE_LR11X0=1 -DMESHTASTIC_EXCLUDE_WIFI=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak_wismeshtag> -lib_deps = - ${nrf52840_base.lib_deps} \ No newline at end of file diff --git a/variants/nrf52840/rak_wismeshtag/variant.cpp b/variants/nrf52840/rak_wismeshtag/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.cpp +++ b/variants/nrf52840/rak_wismeshtag/variant.cpp @@ -36,9 +36,6 @@ void initVariant() 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/nrf52840/rak_wismeshtag/variant.h b/variants/nrf52840/rak_wismeshtag/variant.h index 159cabf07..5b20e4d93 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.h +++ b/variants/nrf52840/rak_wismeshtag/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -62,8 +59,6 @@ extern "C" { #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 @@ -234,6 +229,8 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define RAK_4631 1 +#define HAS_SCREEN 0 + #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/rak_wismeshtap/platformio.ini b/variants/nrf52840/rak_wismeshtap/platformio.ini index 3369f9c77..f058d9153 100644 --- a/variants/nrf52840/rak_wismeshtap/platformio.ini +++ b/variants/nrf52840/rak_wismeshtap/platformio.ini @@ -1,5 +1,14 @@ ; The very slick RAK wireless RAK10701 Field Tester device. Note you will have to flash to Arduino bootloader to use this firmware. Be aware touch is not currently working. [env:rak_wismeshtap] +custom_meshtastic_hw_model = 84 +custom_meshtastic_hw_model_slug = WISMESH_TAP +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = RAK WisMesh Tap +custom_meshtastic_images = rak-wismeshtap.svg +custom_meshtastic_tags = RAK + extends = nrf52840_base board = wiscore_rak4631 build_flags = ${nrf52840_base.build_flags} @@ -17,14 +26,21 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak_wismeshtap> + + + lib_deps = ${nrf52840_base.lib_deps} - ${nrf52_networking_base.lib_deps} - melopero/Melopero RV3028@^1.1.0 + ${networking_base.lib_deps} + # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 + melopero/Melopero RV3028@1.2.0 + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip - rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 - bodmer/TFT_eSPI - beegee-tokyo/RAKwireless RAK12034@^1.0.0 - beegee-tokyo/RAK14014-FT6336U @ 1.0.1 - beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 + # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library + rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 + # renovate: datasource=custom.pio depName=TFT_eSPI packageName=bodmer/library/TFT_eSPI + bodmer/TFT_eSPI@2.5.43 + # renovate: datasource=custom.pio depName=RAK12034 packageName=beegee-tokyo/library/RAKwireless RAK12034 + beegee-tokyo/RAKwireless RAK12034@1.0.0 + # renovate: datasource=custom.pio depName=RAK14014-FT6336U packageName=beegee-tokyo/library/RAK14014-FT6336U + beegee-tokyo/RAK14014-FT6336U@1.0.1 + # renovate: datasource=custom.pio depName=RAK12035_SoilMoisture packageName=beegee-tokyo/library/RAK12035_SoilMoisture + 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/nrf52840/rak_wismeshtap/variant.cpp b/variants/nrf52840/rak_wismeshtap/variant.cpp index 5a3587982..73d2fac04 100644 --- a/variants/nrf52840/rak_wismeshtap/variant.cpp +++ b/variants/nrf52840/rak_wismeshtap/variant.cpp @@ -36,10 +36,24 @@ void initVariant() 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); +} + +void variant_shutdown() +{ + // GPIO restores input status, otherwise there will be leakage current + nrf_gpio_cfg_default(TFT_BL); + nrf_gpio_cfg_default(TFT_DC); + nrf_gpio_cfg_default(TFT_CS); + nrf_gpio_cfg_default(TFT_SCLK); + nrf_gpio_cfg_default(TFT_MOSI); + nrf_gpio_cfg_default(TFT_MISO); + 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); } \ No newline at end of file diff --git a/variants/nrf52840/rak_wismeshtap/variant.h b/variants/nrf52840/rak_wismeshtap/variant.h index a7b9290a5..358117cd5 100644 --- a/variants/nrf52840/rak_wismeshtap/variant.h +++ b/variants/nrf52840/rak_wismeshtap/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -62,8 +59,6 @@ extern "C" { #define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion such as the RAK14014 or RAK14015 TFT modules #define BUTTON_NEED_PULLUP #define PIN_BUTTON2 12 -#define PIN_BUTTON3 24 -#define PIN_BUTTON4 25 /* * Analog pins @@ -271,8 +266,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (1.73F) -#define HAS_RTC 1 - #define RAK_4631 1 #define AQ_SET_PIN 10 @@ -283,7 +276,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define RAK14014 // Tell it we have a RAK14014 #define USER_SETUP_LOADED 1 -#define DISABLE_ALL_LIBRARY_WARNINGS 1 #define ST7789_DRIVER 1 #define TFT_WIDTH 240 #define TFT_HEIGHT 320 @@ -311,7 +303,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define USE_POWERSAVE #define SLEEP_TIME 120 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define USE_VIRTUAL_KEYBOARD 1 /*---------------------------------------------------------------------------- * Arduino objects - C++ only diff --git a/variants/nrf52840/seeed_solar_node/platformio.ini b/variants/nrf52840/seeed_solar_node/platformio.ini index b2a128c57..18894c049 100644 --- a/variants/nrf52840/seeed_solar_node/platformio.ini +++ b/variants/nrf52840/seeed_solar_node/platformio.ini @@ -1,4 +1,13 @@ [env:seeed_solar_node] +custom_meshtastic_hw_model = 95 +custom_meshtastic_hw_model_slug = SEEED_SOLAR_NODE +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Seeed SenseCAP Solar Node +custom_meshtastic_images = seeed_solar.svg +custom_meshtastic_tags = Seeed + board = seeed_solar_node extends = nrf52840_base ;board_level = extra @@ -9,6 +18,4 @@ build_flags = ${nrf52840_base.build_flags} -I src/platform/nrf52/softdevice/nrf52 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_solar_node> -lib_deps = - ${nrf52840_base.lib_deps} debug_tool = jlink diff --git a/variants/nrf52840/seeed_solar_node/variant.h b/variants/nrf52840/seeed_solar_node/variant.h index 7b7738547..69736a9e0 100644 --- a/variants/nrf52840/seeed_solar_node/variant.h +++ b/variants/nrf52840/seeed_solar_node/variant.h @@ -23,12 +23,8 @@ #define PIN_LED1 (12) // LED P1.15 #define PIN_LED2 (11) // -#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 @@ -116,13 +112,13 @@ static const uint8_t SCL = PIN_WIRE_SCL; // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define GPS_L76K #ifdef GPS_L76K -#define PIN_GPS_TX D6 // 44 -#define PIN_GPS_RX D7 // 43 +#define GPS_TX_PIN D6 // 44 +#define GPS_RX_PIN D7 // 43 #define HAS_GPS 1 #define GPS_BAUDRATE 9600 #define GPS_THREAD_INTERVAL 50 -#define PIN_SERIAL1_TX PIN_GPS_TX -#define PIN_SERIAL1_RX PIN_GPS_RX +#define PIN_SERIAL1_TX GPS_TX_PIN +#define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_GPS_STANDBY D0 #define GPS_EN D18 // P1.05 #endif diff --git a/variants/nrf52840/seeed_wio_tracker_L1/platformio.ini b/variants/nrf52840/seeed_wio_tracker_L1/platformio.ini index 6c137384d..d5b56b7bf 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1/platformio.ini +++ b/variants/nrf52840/seeed_wio_tracker_L1/platformio.ini @@ -1,4 +1,14 @@ [env:seeed_wio_tracker_L1] +custom_meshtastic_hw_model = 99 +custom_meshtastic_hw_model_slug = SEEED_WIO_TRACKER_L1 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Seeed Wio Tracker L1 +custom_meshtastic_images = wio_tracker_l1_case.svg +custom_meshtastic_tags = Seeed +custom_meshtastic_requires_dfu = true + board = seeed_wio_tracker_L1 extends = nrf52840_base build_flags = ${nrf52840_base.build_flags} @@ -8,6 +18,4 @@ build_flags = ${nrf52840_base.build_flags} -I src/platform/nrf52/softdevice/nrf52 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1> -lib_deps = - ${nrf52840_base.lib_deps} debug_tool = jlink diff --git a/variants/nrf52840/seeed_wio_tracker_L1/variant.h b/variants/nrf52840/seeed_wio_tracker_L1/variant.h index c5647caa8..a1ec2508a 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1/variant.h +++ b/variants/nrf52840/seeed_wio_tracker_L1/variant.h @@ -23,13 +23,9 @@ #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 +#define LED_STATE_ON 1 // State when LED is lit // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Button Configuration // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -119,16 +115,14 @@ static const uint8_t SCL = PIN_WIRE_SCL; // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define GPS_L76K #ifdef GPS_L76K -#define PIN_GPS_RX D6 // P0.26 -#define PIN_GPS_TX D7 +#define GPS_TX_PIN D6 // P0.26 - This is data from the MCU +#define GPS_RX_PIN D7 // P0.27 - This is data from the GNSS #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_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN -#define GPS_RX_PIN PIN_GPS_TX -#define GPS_TX_PIN PIN_GPS_RX #define PIN_GPS_STANDBY D0 // #define GPS_DEBUG @@ -160,8 +154,6 @@ static const uint8_t SCL = PIN_WIRE_SCL; // joystick // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -#define CANNED_MESSAGE_MODULE_ENABLE 1 - #define CANNED_MESSAGE_ADD_CONFIRMATION 1 // trackball diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h index 7fb890303..98aeb8700 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h @@ -19,7 +19,7 @@ // Shared NicheGraphics components // -------------------------------- #include "graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h" -#include "graphics/niche/Inputs/TwoButton.h" +#include "graphics/niche/Inputs/TwoButtonExtended.h" void setupNicheGraphics() { @@ -54,7 +54,12 @@ void setupNicheGraphics() InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings - inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise + inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise +#if HAS_TRACKBALL + inkhud->persistence->settings.joystick.enabled = true; // Device uses a joystick + inkhud->persistence->settings.joystick.alignment = 3; // 270 degrees + inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Use joystick instead +#endif inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side @@ -75,16 +80,36 @@ void setupNicheGraphics() // Buttons // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButtonExtended *buttons = Inputs::TwoButtonExtended::getInstance(); // Shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); +#if HAS_TRACKBALL + // #0: Exit Button + buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); + buttons->setTiming(0, 75, 500); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->exitShort(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->exitLong(); }); + + // #1: Joystick Center + buttons->setWiring(1, TB_PRESS); + buttons->setTiming(1, 75, 500); + buttons->setHandlerShortPress(1, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(1, [inkhud]() { inkhud->longpress(); }); + + // Joystick Directions + buttons->setJoystickWiring(TB_UP, TB_DOWN, TB_LEFT, TB_RIGHT); + buttons->setJoystickDebounce(50); + buttons->setJoystickPressHandlers([inkhud]() { inkhud->navUp(); }, [inkhud]() { inkhud->navDown(); }, + [inkhud]() { inkhud->navLeft(); }, [inkhud]() { inkhud->navRight(); }); +#else + // #0: User Button + buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); buttons->setTiming(0, 75, 500); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); +#endif // Begin handling button events buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini index 7f9eb0e2c..60d83b95a 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini @@ -1,4 +1,13 @@ [env:seeed_wio_tracker_L1_eink] +custom_meshtastic_hw_model = 100 +custom_meshtastic_hw_model_slug = SEEED_WIO_TRACKER_L1_EINK +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Seeed Wio Tracker L1 E-Ink +custom_meshtastic_images = wio_tracker_l1_eink.svg +custom_meshtastic_tags = Seeed + board = seeed_wio_tracker_L1 extends = nrf52840_base ;board_level = extra @@ -24,7 +33,8 @@ board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1_eink> lib_deps = ${nrf52840_base.lib_deps} - https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip debug_tool = jlink [env:seeed_wio_tracker_L1_eink-inkhud] @@ -44,4 +54,4 @@ build_src_filter = lib_deps = ${inkhud.lib_deps} ; Before base libs_deps, so we use ZinggJM/GFXRoot instead of AdafruitGFX (saves space) ${nrf52840_base.lib_deps} -debug_tool = jlink \ No newline at end of file +debug_tool = jlink diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h index 09fefc7f2..495c4ace8 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h @@ -23,13 +23,9 @@ #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 +#define LED_STATE_ON 1 // State when LED is lit // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Button Configuration // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -129,16 +125,14 @@ static const uint8_t SCL = PIN_WIRE_SCL; // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define GPS_L76K #ifdef GPS_L76K -#define PIN_GPS_RX D6 // P0.26 -#define PIN_GPS_TX D7 +#define GPS_TX_PIN D6 // P0.26 - This is data from the MCU +#define GPS_RX_PIN D7 // P0.27 - This is data from the GNSS #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_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN -#define GPS_RX_PIN PIN_GPS_TX -#define GPS_TX_PIN PIN_GPS_RX #define PIN_GPS_STANDBY D0 // #define GPS_DEBUG @@ -179,7 +173,6 @@ static const uint8_t SCL = PIN_WIRE_SCL; #define TB_PRESS 29 #define TB_DIRECTION FALLING -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define CANNED_MESSAGE_ADD_CONFIRMATION 1 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini index 4c68b40e8..68be47622 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini @@ -1,5 +1,14 @@ ; Seeed Xiao BLE: https://wiki.seeedstudio.com/XIAO_BLE/ [env:seeed_xiao_nrf52840_kit] +custom_meshtastic_hw_model = 88 +custom_meshtastic_hw_model_slug = XIAO_NRF52_KIT +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Seeed Xiao NRF52840 Kit +custom_meshtastic_images = seeed_xiao_nrf52_kit.svg +custom_meshtastic_tags = Seeed + extends = nrf52840_base board = xiao_ble_sense board_level = pr @@ -11,8 +20,6 @@ build_flags = ${nrf52840_base.build_flags} -DGPS_L76K board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> -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/nrf52840/seeed_xiao_nrf52840_kit/variant.h b/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h index fb112a302..0d599d313 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h @@ -69,11 +69,6 @@ static const uint8_t A5 = PIN_A5; #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 */ @@ -147,12 +142,12 @@ static const uint8_t SCK = PIN_SPI_SCK; */ // GPS L76K #ifdef GPS_L76K -#define PIN_GPS_TX D6 -#define PIN_GPS_RX D7 +#define GPS_TX_PIN D6 // This is data from the MCU +#define GPS_RX_PIN D7 // This is data from the GNSS module #define HAS_GPS 1 #define GPS_THREAD_INTERVAL 50 -#define PIN_SERIAL1_TX PIN_GPS_TX -#define PIN_SERIAL1_RX PIN_GPS_RX +#define PIN_SERIAL1_TX GPS_TX_PIN +#define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_GPS_STANDBY D0 #else #define PIN_SERIAL1_RX (-1) diff --git a/variants/nrf52840/t-echo-lite/platformio.ini b/variants/nrf52840/t-echo-lite/platformio.ini index 90e6487a7..c873dea37 100644 --- a/variants/nrf52840/t-echo-lite/platformio.ini +++ b/variants/nrf52840/t-echo-lite/platformio.ini @@ -1,5 +1,14 @@ ; Using original screen class [env:t-echo-lite] +custom_meshtastic_hw_model = 109 +custom_meshtastic_hw_model_slug = T_ECHO_LITE +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = false +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Echo Lite +custom_meshtastic_images = techo_lite.svg +custom_meshtastic_tags = LilyGo + extends = nrf52840_base board = t-echo board_check = true @@ -20,5 +29,6 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo-lite> lib_deps = ${nrf52840_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip ;upload_protocol = fs diff --git a/variants/nrf52840/t-echo-lite/variant.h b/variants/nrf52840/t-echo-lite/variant.h index 0748f6d48..54c7bdfb5 100644 --- a/variants/nrf52840/t-echo-lite/variant.h +++ b/variants/nrf52840/t-echo-lite/variant.h @@ -51,9 +51,6 @@ extern "C" { #define LED_GREEN PIN_LED1 #define BLE_LED LED_BLUE -#define BLE_LED_INVERTED 1 -#define LED_BUILTIN LED_GREEN -#define LED_CONN LED_GREEN #define LED_STATE_ON 0 // State when LED is lit // Buttons @@ -171,6 +168,8 @@ static const uint8_t A0 = PIN_A0; #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (2.0F) +#define SERIAL_PRINT_PORT 0 + // #define NO_EXT_GPIO 1 // PINs back side // Batt & solar connector left up corner diff --git a/variants/nrf52840/t-echo-plus/nicheGraphics.h b/variants/nrf52840/t-echo-plus/nicheGraphics.h new file mode 100644 index 000000000..483e16ea4 --- /dev/null +++ b/variants/nrf52840/t-echo-plus/nicheGraphics.h @@ -0,0 +1,70 @@ +#pragma once + +#include "configuration.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" +#include "graphics/niche/Drivers/EInk/GDEY0154D67.h" +#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" +#include "graphics/niche/InkHUD/InkHUD.h" +#include "graphics/niche/Inputs/TwoButton.h" + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + SPI1.begin(); + + Drivers::EInk *driver = new Drivers::GDEY0154D67; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + inkhud->setDriver(driver); + inkhud->setDisplayResilience(20, 1.5); + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + inkhud->persistence->settings.userTiles.maxCount = 2; + inkhud->persistence->settings.rotation = 3; + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; + inkhud->persistence->settings.optionalMenuItems.backlight = true; + + Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); + backlight->setPin(PIN_EINK_BL); + + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); + inkhud->addApplet("DMs", new InkHUD::DMApplet); + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); + + inkhud->begin(); + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); + + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setTiming(0, 75, 500); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + + buttons->setWiring(1, PIN_BUTTON_TOUCH); + buttons->setTiming(1, 50, 5000); + buttons->setHandlerDown(1, [inkhud, backlight]() { + backlight->peek(); + inkhud->persistence->settings.optionalMenuItems.backlight = false; + }); + buttons->setHandlerLongPress(1, [backlight]() { backlight->latch(); }); + buttons->setHandlerShortPress(1, [backlight]() { backlight->off(); }); + + buttons->start(); +} + +#endif diff --git a/variants/nrf52840/t-echo-plus/platformio.ini b/variants/nrf52840/t-echo-plus/platformio.ini new file mode 100644 index 000000000..b77d54748 --- /dev/null +++ b/variants/nrf52840/t-echo-plus/platformio.ini @@ -0,0 +1,26 @@ +[env:t-echo-plus] +extends = nrf52840_base +board = t-echo +board_level = pr +board_check = true +debug_tool = jlink + +build_flags = ${nrf52840_base.build_flags} + -DTTGO_T_ECHO_PLUS + -Ivariants/nrf52840/t-echo-plus + -DEINK_DISPLAY_MODEL=GxEPD2_154_D67 + -DEINK_WIDTH=200 + -DEINK_HEIGHT=200 + -DUSE_EINK + -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk + -DEINK_LIMIT_FASTREFRESH=20 ; How many consecutive fast-refreshes are permitted + -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. + -DI2C_NO_RESCAN + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo-plus> + +lib_deps = + ${nrf52840_base.lib_deps} + https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip + lewisxhe/PCF8563_Library@^1.0.1 + adafruit/Adafruit DRV2605 Library@1.2.4 diff --git a/variants/nrf52840/t-echo-plus/variant.cpp b/variants/nrf52840/t-echo-plus/variant.cpp new file mode 100644 index 000000000..084186bf6 --- /dev/null +++ b/variants/nrf52840/t-echo-plus/variant.cpp @@ -0,0 +1,24 @@ +#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() +{ + // LEDs (if populated) + 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/nrf52840/t-echo-plus/variant.h b/variants/nrf52840/t-echo-plus/variant.h new file mode 100644 index 000000000..4038ce6ef --- /dev/null +++ b/variants/nrf52840/t-echo-plus/variant.h @@ -0,0 +1,144 @@ +#ifndef _VARIANT_T_ECHO_PLUS_ +#define _VARIANT_T_ECHO_PLUS_ + +#define VARIANT_MCK (64000000ul) +#define USE_LFXO + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// Pin counts +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs (not documented on pinmap; keep defaults for compatibility) +#define PIN_LED1 (0 + 14) +#define PIN_LED2 (0 + 15) +#define PIN_LED3 (0 + 13) + +#define LED_RED PIN_LED3 +#define LED_BLUE PIN_LED1 +#define LED_GREEN PIN_LED2 + +#define LED_STATE_ON 0 + +// Buttons / touch +#define PIN_BUTTON1 (32 + 10) +#define BUTTON_ACTIVE_LOW true +#define BUTTON_ACTIVE_PULLUP true +#define PIN_BUTTON2 (0 + 18) // reset-labelled but usable as GPIO +#define PIN_BUTTON_TOUCH (0 + 11) // capacitive touch +#define BUTTON_TOUCH_ACTIVE_LOW true +#define BUTTON_TOUCH_ACTIVE_PULLUP true + +#define BUTTON_CLICK_MS 400 +#define BUTTON_TOUCH_MS 200 + +// Analog +#define PIN_A0 (4) +#define BATTERY_PIN PIN_A0 +static const uint8_t A0 = PIN_A0; +#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 (2.0F) + +// NFC +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +// I2C (IMU BHI260AP, RTC, etc.) +#define WIRE_INTERFACES_COUNT 1 +#define PIN_WIRE_SDA (0 + 26) +#define PIN_WIRE_SCL (0 + 27) +#define HAS_BHI260AP + +#define TP_SER_IO (0 + 11) + +// RTC interrupt +#define PIN_RTC_INT (0 + 16) + +// QSPI flash +#define PIN_QSPI_SCK (32 + 14) +#define PIN_QSPI_CS (32 + 15) +#define PIN_QSPI_IO0 (32 + 12) +#define PIN_QSPI_IO1 (32 + 13) +#define PIN_QSPI_IO2 (0 + 7) +#define PIN_QSPI_IO3 (0 + 5) + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES MX25R1635F +#define EXTERNAL_FLASH_USE_QSPI + +// LoRa SX1262 +#define USE_SX1262 +#define USE_SX1268 +#define SX126X_CS (0 + 24) +#define SX126X_DIO1 (0 + 20) +#define SX1262_DIO3 (0 + 21) +#define SX126X_BUSY (0 + 17) +#define SX126X_RESET (0 + 25) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define TCXO_OPTIONAL + +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (0 + 23) +#define PIN_SPI_MOSI (0 + 22) +#define PIN_SPI_SCK (0 + 19) + +// E-paper (1.54" per pinmap) +// Alias PIN_EINK_EN to keep common eink power control code working +#define PIN_EINK_BL (32 + 11) // backlight / panel power switch +#define PIN_EINK_EN PIN_EINK_BL +#define PIN_EINK_CS (0 + 30) +#define PIN_EINK_BUSY (0 + 3) +#define PIN_EINK_DC (0 + 28) +#define PIN_EINK_RES (0 + 2) +#define PIN_EINK_SCLK (0 + 31) +#define PIN_EINK_MOSI (0 + 29) // also called SDI + +// Power control +#define PIN_POWER_EN (0 + 12) + +#define PIN_SPI1_MISO (32 + 7) // Placeholder MISO; keep off QSPI pins to avoid contention +#define PIN_SPI1_MOSI PIN_EINK_MOSI +#define PIN_SPI1_SCK PIN_EINK_SCLK + +// GPS (TX/RX/Wake/Reset/PPS per pinmap) +#define GPS_L76K +#define PIN_GPS_REINIT (32 + 5) // Reset +#define PIN_GPS_STANDBY (32 + 2) // Wake +#define PIN_GPS_PPS (32 + 4) +#define GPS_TX_PIN (32 + 8) +#define GPS_RX_PIN (32 + 9) +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN + +// Sensors / accessories +#define PIN_BUZZER (0 + 6) +#define PIN_DRV_EN (0 + 8) + +#define HAS_DRV2605 1 + +// Battery / ADC already defined above +#define HAS_RTC 1 + +#define SERIAL_PRINT_PORT 0 + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/variants/nrf52840/t-echo/platformio.ini b/variants/nrf52840/t-echo/platformio.ini index 051fb3099..4acd70b02 100644 --- a/variants/nrf52840/t-echo/platformio.ini +++ b/variants/nrf52840/t-echo/platformio.ini @@ -1,5 +1,14 @@ ; Using original screen class [env:t-echo] +custom_meshtastic_hw_model = 7 +custom_meshtastic_hw_model_slug = T_ECHO +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Echo +custom_meshtastic_images = t-echo.svg +custom_meshtastic_tags = LilyGo + extends = nrf52840_base board = t-echo board_level = pr @@ -20,8 +29,10 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo> lib_deps = ${nrf52840_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 ;upload_protocol = fs [env:t-echo-inkhud] @@ -41,4 +52,5 @@ 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 \ No newline at end of file + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 diff --git a/variants/nrf52840/t-echo/variant.cpp b/variants/nrf52840/t-echo/variant.cpp index cae079b74..cb64530f6 100644 --- a/variants/nrf52840/t-echo/variant.cpp +++ b/variants/nrf52840/t-echo/variant.cpp @@ -42,3 +42,13 @@ void initVariant() pinMode(PIN_LED3, OUTPUT); ledOff(PIN_LED3); } + +void variant_shutdown() +{ + // To power off the T-Echo, the display must be set + // as an input pin; otherwise, there will be leakage current. + pinMode(PIN_EINK_CS, INPUT); + pinMode(PIN_EINK_DC, INPUT); + pinMode(PIN_EINK_RES, INPUT); + pinMode(PIN_EINK_BUSY, INPUT); +} \ No newline at end of file diff --git a/variants/nrf52840/t-echo/variant.h b/variants/nrf52840/t-echo/variant.h index 9a0cd0578..f4644c6de 100644 --- a/variants/nrf52840/t-echo/variant.h +++ b/variants/nrf52840/t-echo/variant.h @@ -52,9 +52,6 @@ extern "C" { #define LED_BLUE PIN_LED1 #define LED_GREEN PIN_LED2 -#define LED_BUILTIN LED_BLUE -#define LED_CONN PIN_GREEN - #define LED_STATE_ON 0 // State when LED is lit /* @@ -88,6 +85,7 @@ static const uint8_t A0 = PIN_A0; /* * Serial interfaces */ +#define SERIAL_PRINT_PORT 0 /* No longer populated on PCB @@ -108,8 +106,6 @@ No longer populated on PCB #define TP_SER_IO (0 + 11) -#define PIN_RTC_INT (0 + 16) // Interrupt from the PCF8563 RTC - /* External serial flash WP25R1635FZUIL0 */ @@ -165,7 +161,6 @@ External serial flash WP25R1635FZUIL0 // Controls power for all peripherals (eink + GPS + LoRa + Sensor) #define PIN_POWER_EN (0 + 12) -// #define PIN_POWER_EN1 (0 + 13) #define PIN_SPI1_MISO \ (32 + 7) // FIXME not really needed, but for now the SPI code requires something to be defined, pick an used GPIO @@ -191,6 +186,7 @@ External serial flash WP25R1635FZUIL0 #define PIN_SERIAL1_TX GPS_TX_PIN // PCF8563 RTC Module +#define PIN_RTC_INT (0 + 16) // Interrupt from the PCF8563 RTC #define PCF8563_RTC 0x51 /* @@ -219,8 +215,6 @@ External serial flash WP25R1635FZUIL0 // #define NO_EXT_GPIO 1 -#define HAS_RTC 1 - #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/tracker-t1000-e/platformio.ini b/variants/nrf52840/tracker-t1000-e/platformio.ini index 905d751fd..43ba7a8b4 100644 --- a/variants/nrf52840/tracker-t1000-e/platformio.ini +++ b/variants/nrf52840/tracker-t1000-e/platformio.ini @@ -1,7 +1,16 @@ [env:tracker-t1000-e] +custom_meshtastic_support_level = 1 +custom_meshtastic_images = tracker-t1000-e.svg +custom_meshtastic_tags = Seeed + extends = nrf52840_base board = tracker-t1000-e board_level = pr +custom_meshtastic_hw_model = 71 +custom_meshtastic_hw_model_slug = TRACKER_T1000_E +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_display_name = Seeed SenseCAP T1000-E +custom_meshtastic_actively_supported = true build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/tracker-t1000-e -Isrc/platform/nrf52/softdevice @@ -16,6 +25,7 @@ board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/tracker-t1000-e> lib_deps = ${nrf52840_base.lib_deps} + # TODO renovate https://github.com/meshtastic/QMA6100P_Arduino_Library/archive/14c900b8b2e4feaac5007a7e41e0c1b7f0841136.zip debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) diff --git a/variants/nrf52840/tracker-t1000-e/variant.h b/variants/nrf52840/tracker-t1000-e/variant.h index 5b6719e12..143b7d4ad 100644 --- a/variants/nrf52840/tracker-t1000-e/variant.h +++ b/variants/nrf52840/tracker-t1000-e/variant.h @@ -48,7 +48,6 @@ extern "C" { #define PIN_LED1 (0 + 24) // P0.24 #define LED_PIN PIN_LED1 -#define LED_BUILTIN -1 #define LED_BLUE -1 // Actually green #define LED_STATE_ON 1 // State when LED is lit diff --git a/variants/nrf52840/wio-sdk-wm1110/variant.h b/variants/nrf52840/wio-sdk-wm1110/variant.h index b6e5c79df..d802d20f6 100644 --- a/variants/nrf52840/wio-sdk-wm1110/variant.h +++ b/variants/nrf52840/wio-sdk-wm1110/variant.h @@ -58,8 +58,6 @@ extern "C" { #define PIN_LED1 (0 + 13) // P0.13 #define PIN_LED2 (0 + 14) // P0.14 -#define LED_BUILTIN PIN_LED1 - #define LED_GREEN PIN_LED1 #define LED_BLUE PIN_LED2 // Actually red diff --git a/variants/nrf52840/wio-t1000-s/platformio.ini b/variants/nrf52840/wio-t1000-s/platformio.ini index c6b61fc8a..a6ea5102c 100644 --- a/variants/nrf52840/wio-t1000-s/platformio.ini +++ b/variants/nrf52840/wio-t1000-s/platformio.ini @@ -10,8 +10,6 @@ build_flags = ${nrf52840_base.build_flags} -DWIO_WM1110 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/wio-t1000-s> -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 \ No newline at end of file diff --git a/variants/nrf52840/wio-t1000-s/variant.h b/variants/nrf52840/wio-t1000-s/variant.h index 02f8a20b2..3b8103d85 100644 --- a/variants/nrf52840/wio-t1000-s/variant.h +++ b/variants/nrf52840/wio-t1000-s/variant.h @@ -48,7 +48,6 @@ extern "C" { #define PIN_LED1 (0 + 24) // P0.24 #define LED_PIN PIN_LED1 -#define LED_BUILTIN -1 #define LED_BLUE -1 // Actually green #define LED_STATE_ON 1 // State when LED is lit diff --git a/variants/nrf52840/wio-tracker-wm1110/platformio.ini b/variants/nrf52840/wio-tracker-wm1110/platformio.ini index 73b7dedd4..515712062 100644 --- a/variants/nrf52840/wio-tracker-wm1110/platformio.ini +++ b/variants/nrf52840/wio-tracker-wm1110/platformio.ini @@ -1,5 +1,15 @@ ; The red tracker Dev Board with the WM1110 module [env:wio-tracker-wm1110] +custom_meshtastic_hw_model = 21 +custom_meshtastic_hw_model_slug = WIO_WM1110 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Seeed Wio WM1110 Tracker +custom_meshtastic_images = wio-tracker-wm1110.svg +custom_meshtastic_tags = Seeed +custom_meshtastic_requires_dfu = true + extends = nrf52840_base board = wio-tracker-wm1110 build_flags = ${nrf52840_base.build_flags} @@ -9,7 +19,5 @@ build_flags = ${nrf52840_base.build_flags} -DWIO_WM1110 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/wio-tracker-wm1110> -lib_deps = - ${nrf52840_base.lib_deps} ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) -;upload_protocol = jlink \ No newline at end of file +;upload_protocol = jlink diff --git a/variants/nrf52840/wio-tracker-wm1110/variant.cpp b/variants/nrf52840/wio-tracker-wm1110/variant.cpp index 5a3587982..0d0856773 100644 --- a/variants/nrf52840/wio-tracker-wm1110/variant.cpp +++ b/variants/nrf52840/wio-tracker-wm1110/variant.cpp @@ -36,9 +36,6 @@ void initVariant() 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/nrf52840/wio-tracker-wm1110/variant.h b/variants/nrf52840/wio-tracker-wm1110/variant.h index 807ca8dbb..647bd47d8 100644 --- a/variants/nrf52840/wio-tracker-wm1110/variant.h +++ b/variants/nrf52840/wio-tracker-wm1110/variant.h @@ -51,13 +51,8 @@ extern "C" { #define PIN_WIRE_SDA (0 + 5) // P0.05 #define PIN_WIRE_SCL (0 + 4) // P0.04 -#define PIN_LED1 (0 + 6) // P0.06 -#define PIN_LED2 (PINS_COUNT) // P0.14 - -#define LED_BUILTIN PIN_LED1 - +#define PIN_LED1 (0 + 6) // P0.06 #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 #define LED_STATE_ON 0 diff --git a/variants/rp2040/challenger_2040_lora/pins_arduino.h b/variants/rp2040/challenger_2040_lora/pins_arduino.h index ac472c07e..b27cc0297 100644 --- a/variants/rp2040/challenger_2040_lora/pins_arduino.h +++ b/variants/rp2040/challenger_2040_lora/pins_arduino.h @@ -7,7 +7,7 @@ #define ADC_RESOLUTION (12u) // LEDs -#define PIN_LED (24u) +#define LED_PIN (24u) // Serial #define PIN_SERIAL1_TX (16u) @@ -45,8 +45,6 @@ #define SPI_HOWMANY (2u) #define WIRE_HOWMANY (1u) -#define LED_BUILTIN PIN_LED - static const uint8_t D0 = (16u); static const uint8_t D1 = (17u); static const uint8_t D2 = (20u); diff --git a/variants/rp2040/challenger_2040_lora/platformio.ini b/variants/rp2040/challenger_2040_lora/platformio.ini index 4a709d650..06371d18e 100644 --- a/variants/rp2040/challenger_2040_lora/platformio.ini +++ b/variants/rp2040/challenger_2040_lora/platformio.ini @@ -10,7 +10,5 @@ build_flags = -I variants/rp2040/challenger_2040_lora -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE -lib_deps = - ${rp2040_base.lib_deps} debug_build_flags = ${rp2040_base.build_flags} debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2040/challenger_2040_lora/variant.h b/variants/rp2040/challenger_2040_lora/variant.h index 552f90720..f5126cfff 100644 --- a/variants/rp2040/challenger_2040_lora/variant.h +++ b/variants/rp2040/challenger_2040_lora/variant.h @@ -5,8 +5,6 @@ #define EXT_NOTIFY_OUT 0xFFFFFFFF #define BUTTON_PIN 0xFFFFFFFF -#define LED_PIN PIN_LED - #define USE_RF95 // RFM95/SX127x #undef LORA_SCK diff --git a/variants/rp2040/ec_catsniffer/platformio.ini b/variants/rp2040/ec_catsniffer/platformio.ini index b70eff6d7..08fa3fffc 100644 --- a/variants/rp2040/ec_catsniffer/platformio.ini +++ b/variants/rp2040/ec_catsniffer/platformio.ini @@ -9,7 +9,5 @@ build_flags = -I variants/rp2040/ec_catsniffer -D DEBUG_RP2040_PORT=Serial ; -D HW_SPI1_DEVICE -lib_deps = - ${rp2040_base.lib_deps} debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap diff --git a/variants/rp2040/feather_rp2040_rfm95/platformio.ini b/variants/rp2040/feather_rp2040_rfm95/platformio.ini index ef4118cb0..1da1218e4 100644 --- a/variants/rp2040/feather_rp2040_rfm95/platformio.ini +++ b/variants/rp2040/feather_rp2040_rfm95/platformio.ini @@ -1,6 +1,7 @@ [env:feather_rp2040_rfm95] extends = rp2040_base board = adafruit_feather +board_level = extra upload_protocol = picotool # add our variants files to the include and src paths build_flags = @@ -9,7 +10,5 @@ build_flags = -I variants/rp2040/feather_rp2040_rfm95 -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE -lib_deps = - ${rp2040_base.lib_deps} debug_build_flags = ${rp2040_base.build_flags} debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2040/nibble_rp2040/platformio.ini b/variants/rp2040/nibble_rp2040/platformio.ini index 024a72206..5f4025cff 100644 --- a/variants/rp2040/nibble_rp2040/platformio.ini +++ b/variants/rp2040/nibble_rp2040/platformio.ini @@ -10,7 +10,5 @@ build_flags = -I variants/rp2040/nibble_rp2040 -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE -lib_deps = - ${rp2040_base.lib_deps} debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2040/rak11310/pins_arduino.h b/variants/rp2040/rak11310/pins_arduino.h index 0e2808b19..59290bbdb 100644 --- a/variants/rp2040/rak11310/pins_arduino.h +++ b/variants/rp2040/rak11310/pins_arduino.h @@ -23,10 +23,8 @@ static const uint8_t A2 = PIN_A2; static const uint8_t A3 = PIN_A3; // LEDs -#define PIN_LED (23u) -#define PIN_LED1 PIN_LED -#define PIN_LED2 (24u) -#define LED_BUILTIN PIN_LED +#define PIN_LED1 (23u) +#define LED_NOTIFICATION (24u) #define ADC_RESOLUTION 12 diff --git a/variants/rp2040/rak11310/platformio.ini b/variants/rp2040/rak11310/platformio.ini index f3eaa176e..2c2b2a4bf 100644 --- a/variants/rp2040/rak11310/platformio.ini +++ b/variants/rp2040/rak11310/platformio.ini @@ -1,4 +1,14 @@ [env:rak11310] +custom_meshtastic_hw_model = 26 +custom_meshtastic_hw_model_slug = RAK11310 +custom_meshtastic_architecture = rp2040 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 2 +custom_meshtastic_display_name = RAK WisBlock 11310 +custom_meshtastic_images = rak11310.svg +custom_meshtastic_tags = RAK +custom_meshtastic_requires_dfu = true + extends = rp2040_base board = rakwireless_rak11300 board_level = pr @@ -14,7 +24,10 @@ build_src_filter = ${rp2040_base.build_src_filter} +<../variants/rp2040/rak11310 lib_deps = ${rp2040_base.lib_deps} ${networking_base.lib_deps} - melopero/Melopero RV3028@^1.1.0 + ${networking_extra.lib_deps} + # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 + melopero/Melopero RV3028@1.2.0 + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2040/rak11310/variant.h b/variants/rp2040/rak11310/variant.h index 2400d56a7..cf49ff491 100644 --- a/variants/rp2040/rak11310/variant.h +++ b/variants/rp2040/rak11310/variant.h @@ -10,8 +10,7 @@ #define I2C_SDA1 2 #define I2C_SCL1 3 -#define LED_CONN PIN_LED2 -#define LED_PIN LED_BUILTIN +#define LED_PIN PIN_LED1 #define ledOff(pin) pinMode(pin, INPUT) #define BUTTON_PIN 9 diff --git a/variants/rp2040/rp2040-lora/platformio.ini b/variants/rp2040/rp2040-lora/platformio.ini index d59e74f20..f1e0b9af6 100644 --- a/variants/rp2040/rp2040-lora/platformio.ini +++ b/variants/rp2040/rp2040-lora/platformio.ini @@ -1,4 +1,13 @@ [env:rp2040-lora] +custom_meshtastic_hw_model = 30 +custom_meshtastic_hw_model_slug = RP2040_LORA +custom_meshtastic_architecture = rp2040 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 2 +custom_meshtastic_display_name = RP2040 LoRa +custom_meshtastic_tags = Waveshare +custom_meshtastic_requires_dfu = true + extends = rp2040_base board = rpipico upload_protocol = picotool @@ -9,7 +18,5 @@ build_flags = -I variants/rp2040/rp2040-lora -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE -lib_deps = - ${rp2040_base.lib_deps} debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2040/rp2040.ini b/variants/rp2040/rp2040.ini index 4f9421872..9abfcbe10 100644 --- a/variants/rp2040/rp2040.ini +++ b/variants/rp2040/rp2040.ini @@ -2,12 +2,12 @@ [rp2040_base] platform = # TODO renovate - https://github.com/maxgerhardt/platform-raspberrypi#76ecf3c7e9dd4503af0331154c4ca1cddc4b03e5 - ; For arduino-pico >= 4.4.3 + https://github.com/maxgerhardt/platform-raspberrypi#cc24cfef37ed22ca9f2a6aead28c2deb76c39f24 + ; For arduino-pico >= 5.4.4 extends = arduino_base platform_packages = # TODO renovate - framework-arduinopico@https://github.com/earlephilhower/arduino-pico#4.4.3 + arduino-pico@https://github.com/earlephilhower/arduino-pico/releases/download/5.4.4/rp2040-5.4.4.zip board_build.core = earlephilhower board_build.filesystem_size = 0.5m @@ -17,6 +17,7 @@ build_flags = -Isrc/platform/rp2xx0/hardware_rosc/include -Isrc/platform/rp2xx0/pico_sleep/include -D__PLAT_RP2040__ + -D__FREERTOS=1 # -D _POSIX_THREADS build_src_filter = ${arduino_base.build_src_filter} - - - - - - - - - diff --git a/variants/rp2040/rpipico-slowclock/platformio.ini b/variants/rp2040/rpipico-slowclock/platformio.ini index 30928aead..d5f86b0ad 100644 --- a/variants/rp2040/rpipico-slowclock/platformio.ini +++ b/variants/rp2040/rpipico-slowclock/platformio.ini @@ -21,8 +21,6 @@ build_flags = -DHW_SPI1_DEVICE -g -DNO_USB -lib_deps = - ${rp2040_base.lib_deps} debug_build_flags = ${rp2040_base.build_flags} -g -DNO_USB diff --git a/variants/rp2040/rpipico/platformio.ini b/variants/rp2040/rpipico/platformio.ini index a6171bbac..4ae134b28 100644 --- a/variants/rp2040/rpipico/platformio.ini +++ b/variants/rp2040/rpipico/platformio.ini @@ -1,9 +1,18 @@ [env:pico] +custom_meshtastic_hw_model = 47 +custom_meshtastic_hw_model_slug = RPI_PICO +custom_meshtastic_architecture = rp2040 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Raspberry Pi Pico +custom_meshtastic_images = pico.svg +custom_meshtastic_tags = RPi, DIY +custom_meshtastic_requires_dfu = true + extends = rp2040_base board = rpipico board_level = pr upload_protocol = picotool - # add our variants files to the include and src paths build_flags = ${rp2040_base.build_flags} @@ -11,7 +20,5 @@ build_flags = -I variants/rp2040/rpipico -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE -lib_deps = - ${rp2040_base.lib_deps} debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2040/rpipicow/platformio.ini b/variants/rp2040/rpipicow/platformio.ini index 60845ba39..9b4b29a5b 100644 --- a/variants/rp2040/rpipicow/platformio.ini +++ b/variants/rp2040/rpipicow/platformio.ini @@ -1,4 +1,14 @@ [env:picow] +custom_meshtastic_hw_model = 47 +custom_meshtastic_hw_model_slug = RPI_PICO +custom_meshtastic_architecture = rp2040 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Raspberry Pi Pico W +custom_meshtastic_images = rpipicow.svg +custom_meshtastic_tags = RPi, DIY +custom_meshtastic_requires_dfu = true + extends = rp2040_base board = rpipicow board_level = pr @@ -12,9 +22,11 @@ build_flags = -D HW_SPI1_DEVICE -D HAS_UDP_MULTICAST=1 -fexceptions # for exception handling in MQTT + -ULED_BUILTIN build_src_filter = ${rp2040_base.build_src_filter} + lib_deps = ${rp2040_base.lib_deps} ${networking_base.lib_deps} + ${networking_extra.lib_deps} debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2040/rpipicow/variant.h b/variants/rp2040/rpipicow/variant.h index 24da8f932..fe94e615d 100644 --- a/variants/rp2040/rpipicow/variant.h +++ b/variants/rp2040/rpipicow/variant.h @@ -19,7 +19,7 @@ #define EXT_NOTIFY_OUT 22 #define BUTTON_PIN 17 -#define LED_PIN LED_BUILTIN +#define LED_PIN PIN_LED #define BATTERY_PIN 26 // ratio of voltage divider = 3.0 (R17=200k, R18=100k) diff --git a/variants/rp2040/senselora_rp2040/pins_arduino.h b/variants/rp2040/senselora_rp2040/pins_arduino.h index bb0ee637e..575839cbc 100644 --- a/variants/rp2040/senselora_rp2040/pins_arduino.h +++ b/variants/rp2040/senselora_rp2040/pins_arduino.h @@ -11,9 +11,7 @@ static const uint8_t A2 = PIN_A2; static const uint8_t A3 = PIN_A3; // LEDs -#define PIN_LED (23u) -#define PIN_LED1 PIN_LED -#define LED_BUILTIN PIN_LED +#define PIN_LED1 (23u) #define ADC_RESOLUTION 12 diff --git a/variants/rp2040/senselora_rp2040/platformio.ini b/variants/rp2040/senselora_rp2040/platformio.ini index 3a574d0f9..e02fd587b 100644 --- a/variants/rp2040/senselora_rp2040/platformio.ini +++ b/variants/rp2040/senselora_rp2040/platformio.ini @@ -9,5 +9,3 @@ build_flags = ${rp2040_base.build_flags} -D SENSELORA_RP2040 -I variants/rp2040/senselora_rp2040 -D DEBUG_RP2040_PORT=Serial -lib_deps = - ${rp2040_base.lib_deps} \ No newline at end of file diff --git a/variants/rp2040/senselora_rp2040/variant.h b/variants/rp2040/senselora_rp2040/variant.h index cc90284b7..04e21e073 100644 --- a/variants/rp2040/senselora_rp2040/variant.h +++ b/variants/rp2040/senselora_rp2040/variant.h @@ -5,7 +5,7 @@ #define BUTTON_PIN 2 #define BUTTON_NEED_PULLUP -#define LED_PIN PIN_LED +#define LED_PIN PIN_LED1 #define ledOff(pin) pinMode(pin, INPUT) #undef BATTERY_PIN diff --git a/variants/rp2350/rp2350.ini b/variants/rp2350/rp2350.ini index e8611a113..934875c6a 100644 --- a/variants/rp2350/rp2350.ini +++ b/variants/rp2350/rp2350.ini @@ -2,12 +2,12 @@ [rp2350_base] platform = # TODO renovate - https://github.com/maxgerhardt/platform-raspberrypi#76ecf3c7e9dd4503af0331154c4ca1cddc4b03e5 - ; For arduino-pico >= 4.4.3 + https://github.com/maxgerhardt/platform-raspberrypi#cc24cfef37ed22ca9f2a6aead28c2deb76c39f24 + ; For arduino-pico >= 5.4.4 extends = arduino_base platform_packages = # TODO renovate - framework-arduinopico@https://github.com/earlephilhower/arduino-pico#4.4.3 + arduino-pico@https://github.com/earlephilhower/arduino-pico/releases/download/5.4.4/rp2040-5.4.4.zip board_build.core = earlephilhower board_build.filesystem_size = 0.5m @@ -15,6 +15,7 @@ build_flags = ${arduino_base.build_flags} -Wno-unused-variable -Wcast-align -Isrc/platform/rp2xx0 -D__PLAT_RP2350__ + -D__FREERTOS=1 build_src_filter = ${arduino_base.build_src_filter} - - - - - - - - - - - diff --git a/variants/rp2350/rpipico2/platformio.ini b/variants/rp2350/rpipico2/platformio.ini index ad7a4ce51..30dc15256 100644 --- a/variants/rp2350/rpipico2/platformio.ini +++ b/variants/rp2350/rpipico2/platformio.ini @@ -11,7 +11,5 @@ build_flags = -I variants/rp2350/rpipico2 -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE -lib_deps = - ${rp2350_base.lib_deps} debug_build_flags = ${rp2350_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2350/rpipico2w/platformio.ini b/variants/rp2350/rpipico2w/platformio.ini index 5dbce533b..da408b67d 100644 --- a/variants/rp2350/rpipico2w/platformio.ini +++ b/variants/rp2350/rpipico2w/platformio.ini @@ -30,4 +30,5 @@ build_src_filter = ${rp2350_base.build_src_filter} + lib_deps = ${rp2350_base.lib_deps} ${networking_base.lib_deps} + ${networking_extra.lib_deps} debug_build_flags = ${rp2350_base.build_flags}, -g diff --git a/variants/stm32/CDEBYTE_E77-MBL/platformio.ini b/variants/stm32/CDEBYTE_E77-MBL/platformio.ini index c5af9a4a4..b4c0c958f 100644 --- a/variants/stm32/CDEBYTE_E77-MBL/platformio.ini +++ b/variants/stm32/CDEBYTE_E77-MBL/platformio.ini @@ -13,6 +13,7 @@ build_flags = -DPIN_SERIAL1_RX=PB7 -DPIN_SERIAL1_TX=PB6 -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 -DMESHTASTIC_EXCLUDE_I2C=1 -DMESHTASTIC_EXCLUDE_GPS=1 diff --git a/variants/stm32/CDEBYTE_E77-MBL/variant.h b/variants/stm32/CDEBYTE_E77-MBL/variant.h index e3d111a33..2cf355a61 100644 --- a/variants/stm32/CDEBYTE_E77-MBL/variant.h +++ b/variants/stm32/CDEBYTE_E77-MBL/variant.h @@ -19,5 +19,7 @@ Do not expect a working Meshtastic device with this target. // #define LED_PIN PB3 // LED2 #define LED_STATE_ON 1 +#define SERIAL_PRINT_PORT 1 + #define EBYTE_E77_MBL #endif diff --git a/variants/stm32/milesight_gs301/platformio.ini b/variants/stm32/milesight_gs301/platformio.ini new file mode 100644 index 000000000..73b9cf7ea --- /dev/null +++ b/variants/stm32/milesight_gs301/platformio.ini @@ -0,0 +1,24 @@ +; Milesight GS301 Bathroom Odor Detector +; https://www.milesight.com/iot/product/lorawan-sensor/gs301 +[env:milesight_gs301] +extends = stm32_base +board = wiscore_rak3172 ; Convenient choice as the same USART is used for programming/debug +board_level = extra +board_upload.maximum_size = 233472 ; reserve the last 28KB for filesystem +build_flags = + ${stm32_base.build_flags} + -Ivariants/stm32/milesight_gs301 + -DPRIVATE_HW + -DMESHTASTIC_EXCLUDE_GPS=1 + -DMESHTASTIC_EXCLUDE_I2C=1 # Analog ADuCM355 (unsupported) so no point building support for I2C in + -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 +build_unflags = + -DDEBUG_MUTE # We have space for debug output until sensor support is added +build_src_filter = + ${stm32_base.build_src_filter} + +<../variants/stm32/milesight_gs301> +lib_deps = + ${stm32_base.lib_deps} + +upload_port = stlink diff --git a/variants/stm32/milesight_gs301/rfswitch.h b/variants/stm32/milesight_gs301/rfswitch.h new file mode 100644 index 000000000..d9f60038a --- /dev/null +++ b/variants/stm32/milesight_gs301/rfswitch.h @@ -0,0 +1,7 @@ +// Seems to use the same RF switch pins as RAK3172… getting Tx/Rx SNR +11dB with a nearby node +// PB8, PC13 + +static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PB8, PC13, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +static const Module::RfSwitchMode_t rfswitch_table[4] = { + {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE}; diff --git a/variants/stm32/milesight_gs301/variant.cpp b/variants/stm32/milesight_gs301/variant.cpp new file mode 100644 index 000000000..ef90d5a54 --- /dev/null +++ b/variants/stm32/milesight_gs301/variant.cpp @@ -0,0 +1,8 @@ +#include "variant.h" +#include "Arduino.h" + +void earlyInitVariant() +{ + pinMode(USER_LED, OUTPUT); + digitalWrite(USER_LED, HIGH ^ LED_STATE_ON); +} \ No newline at end of file diff --git a/variants/stm32/milesight_gs301/variant.h b/variants/stm32/milesight_gs301/variant.h new file mode 100644 index 000000000..e86e93fc4 --- /dev/null +++ b/variants/stm32/milesight_gs301/variant.h @@ -0,0 +1,41 @@ +#ifndef _VARIANT_MILESIGHT_GS301_ +#define _VARIANT_MILESIGHT_GS301_ + +#define USE_STM32WLx + +// I/O +#define LED_STATE_ON 1 +#define PIN_LED1 PA0 // Green LED +#define LED_PIN PIN_LED1 +#define PIN_LED2 PA0 // Red LED +#define USER_LED PIN_LED2 +#define BUTTON_PIN PC13 +#define BUTTON_ACTIVE_LOW true +#define BUTTON_ACTIVE_PULLUP false +#define PIN_BUZZER PA6 + +// EC Sense DGM10 Double Gas Module +// Analog ADuCM355 (unsupported); SHTC3 is connected to ADuCM355 and not directly accessible +#define PIN_WIRE_SDA PB7 +#define PIN_WIRE_SCL PB8 +// Commented out to keep sensor powered down due to lack of support +/* +#define VEXT_ENABLE PB12 // TI TPS61291DRV VSEL - set LOW before ENable for Vout = 3.3V +#define VEXT_ON_VALUE LOW +#define SENSOR_POWER_CTRL_PIN PB2 // TI TPS61291DRV EN pin +#define SENSOR_POWER_ON HIGH +#define HAS_SENSOR 1 +*/ + +#define ENABLE_HWSERIAL1 +#define PIN_SERIAL1_RX NC +#define PIN_SERIAL1_TX PB6 + +// LoRa +#define SX126X_DIO3_TCXO_VOLTAGE 3.0 + +// Required to avoid Serial1 conflicts due to board definition here: +// https://github.com/stm32duino/Arduino_Core_STM32/blob/main/variants/STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U/variant_RAK3172_MODULE.h +#define RAK3172 + +#endif diff --git a/variants/stm32/rak3172/platformio.ini b/variants/stm32/rak3172/platformio.ini index b9a4b8a04..4d96e98f9 100644 --- a/variants/stm32/rak3172/platformio.ini +++ b/variants/stm32/rak3172/platformio.ini @@ -12,6 +12,7 @@ build_flags = -DPIN_WIRE_SDA=PA11 -DPIN_WIRE_SCL=PA12 -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 -DMESHTASTIC_EXCLUDE_I2C=1 -DMESHTASTIC_EXCLUDE_GPS=1 diff --git a/variants/stm32/rak3172/variant.h b/variants/stm32/rak3172/variant.h index 30d2b57b4..b3f6cbcda 100644 --- a/variants/stm32/rak3172/variant.h +++ b/variants/stm32/rak3172/variant.h @@ -17,5 +17,6 @@ Do not expect a working Meshtastic device with this target. #define LED_STATE_ON 1 #define RAK3172 +#define SERIAL_PRINT_PORT 1 #endif diff --git a/variants/stm32/russell/platformio.ini b/variants/stm32/russell/platformio.ini new file mode 100644 index 000000000..0dd57a2c7 --- /dev/null +++ b/variants/stm32/russell/platformio.ini @@ -0,0 +1,21 @@ +; Russell is a board designed to mount on an ER34615/IFR32700 cell and go Up! on a balloon +; Hardware repository: https://github.com/Meshtastic-Malaysia/russell +; - RAK3172 STM32WLE5CCU6 MCU + integrated SX1262 LoRa +; - CDtop CD-PA1010D GPS +; - Bosch Sensortec BME280 sensor +; - Consonance CN3158 LiFePO4 solar charger +[env:russell] +extends = stm32_base +board = wiscore_rak3172 +board_level = extra +board_upload.maximum_size = 233472 ; reserve the last 28KB for filesystem +build_flags = + ${stm32_base.build_flags} + -Ivariants/stm32/russell + -DPRIVATE_HW +lib_deps = + ${stm32_base.lib_deps} + # renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library + adafruit/Adafruit BME280 Library@2.3.0 + +upload_port = stlink diff --git a/variants/stm32/russell/rfswitch.h b/variants/stm32/russell/rfswitch.h new file mode 100644 index 000000000..ec4829de6 --- /dev/null +++ b/variants/stm32/russell/rfswitch.h @@ -0,0 +1,7 @@ +// Pins from https://forum.rakwireless.com/t/rak3172-internal-schematic/4557/2 +// PB8, PC13 + +static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PB8, PC13, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +static const Module::RfSwitchMode_t rfswitch_table[4] = { + {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE}; diff --git a/variants/stm32/russell/variant.h b/variants/stm32/russell/variant.h new file mode 100644 index 000000000..796302d34 --- /dev/null +++ b/variants/stm32/russell/variant.h @@ -0,0 +1,41 @@ +#ifndef _VARIANT_RUSSELL_ +#define _VARIANT_RUSSELL_ + +#define USE_STM32WLx + +// I/O +#define LED_PIN PA0 // Red LED +#define LED_STATE_ON 1 +#define BUTTON_PIN PH3 // Shared with BOOT0 +#define BUTTON_NEED_PULLUP +// Charger IC charge/standby pins are open-drain with no hardware pull-up: +// Internal pull-up is needed on STM32 (TODO) +// #define EXT_CHRG_DETECT PA5 +// #define EXT_PWR_DETECT PA4 + +// Bosch Sensortec BME280 +#define HAS_SENSOR 1 + +// CDtop CD-PA1010D +#define ENABLE_HWSERIAL1 +#define PIN_SERIAL1_RX PB7 +#define PIN_SERIAL1_TX PB6 +#define HAS_GPS 1 +#define PIN_GPS_STANDBY PA15 +#define GPS_RX_PIN PB7 +#define GPS_TX_PIN PB6 + +// LoRa +/* + * RAK3172 (-20–85°C) -> No TCXO + * RAK3172-T (-40–85°C) -> 3.0V TCXO + * https://github.com/RAKWireless/RAK-STM32-RUI/blob/e5a28be8fab1a492bd9223dd425ca33a8a297d90/variants/WisDuo_RAK3172-T_Board/radio_conf.h#L91 + */ +#define TCXO_OPTIONAL +#define SX126X_DIO3_TCXO_VOLTAGE 3.0 + +// Required to avoid Serial1 conflicts due to board definition here: +// https://github.com/stm32duino/Arduino_Core_STM32/blob/main/variants/STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U/variant_RAK3172_MODULE.h +#define RAK3172 + +#endif diff --git a/variants/stm32/stm32.ini b/variants/stm32/stm32.ini index 547b0502e..bb0a4d3ce 100644 --- a/variants/stm32/stm32.ini +++ b/variants/stm32/stm32.ini @@ -4,7 +4,7 @@ platform = # renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32 platformio/ststm32@19.4.0 platform_packages = - # TODO renovate + # renovate: datasource=github-tags depName=Arduino_Core_STM32 packageName=stm32duino/Arduino_Core_STM32 platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip extra_scripts = ${env.extra_scripts} @@ -51,7 +51,6 @@ debug_tool = stlink lib_deps = ${env.lib_deps} ${radiolib_base.lib_deps} - # renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main https://github.com/caveman99/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip diff --git a/variants/stm32/wio-e5/platformio.ini b/variants/stm32/wio-e5/platformio.ini index a9fcf51d6..311cade58 100644 --- a/variants/stm32/wio-e5/platformio.ini +++ b/variants/stm32/wio-e5/platformio.ini @@ -19,7 +19,3 @@ build_flags = -DGPS_SERIAL_PORT=Serial2 upload_port = stlink - -lib_deps = - ${stm32_base.lib_deps} - # Add your custom sensor here! \ No newline at end of file diff --git a/variants/stm32/wio-e5/variant.h b/variants/stm32/wio-e5/variant.h index a312b31bd..2b20eb2a6 100644 --- a/variants/stm32/wio-e5/variant.h +++ b/variants/stm32/wio-e5/variant.h @@ -19,9 +19,4 @@ Do not expect a working Meshtastic device with this target. #define WIO_E5 -#if (defined(LED_BUILTIN) && LED_BUILTIN == PNUM_NOT_DEFINED) -#undef LED_BUILTIN -#define LED_BUILTIN (LED_PIN) -#endif - #endif diff --git a/version.properties b/version.properties index 8e40687e9..62145da14 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 17 +build = 19