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/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml index 19381e211..23690766a 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -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,8 +36,66 @@ 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: 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: diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 4f7cbf194..6b48e8128 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -201,6 +201,7 @@ jobs: ./device-*.bat ./littlefs-*.bin ./bleota*bin + ./mt-*-ota.bin ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 @@ -293,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 @@ -302,8 +321,7 @@ 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@v7 @@ -427,6 +445,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v6 @@ -446,6 +466,13 @@ jobs: 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/release_channels.yml b/.github/workflows/release_channels.yml index badbb31d4..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' diff --git a/.gitignore b/.gitignore index 06e8c472f..769603202 100644 --- a/.gitignore +++ b/.gitignore @@ -48,5 +48,5 @@ arduino-lib-builder* dependencies.lock idf_component.yml CMakeLists.txt -sdkconfig.* +/sdkconfig.* .dummy/* diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 54a803206..12e6696c0 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,10 +9,10 @@ plugins: lint: enabled: - checkov@3.2.497 - - renovate@42.75.0 - - prettier@3.7.4 + - renovate@42.81.8 + - prettier@3.8.0 - trufflehog@3.92.4 - - yamllint@1.37.1 + - yamllint@1.38.0 - bandit@1.9.2 - trivy@0.68.2 - taplo@0.10.0 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/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/config-dist.yaml b/bin/config-dist.yaml index adf804ba9..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 diff --git a/bin/config.d/lora-usb-umesh-1262.yaml b/bin/config.d/lora-usb-umesh-1262.yaml new file mode 100644 index 000000000..6008e63b7 --- /dev/null +++ b/bin/config.d/lora-usb-umesh-1262.yaml @@ -0,0 +1,15 @@ +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: 30 +# Reduce output power to improve EMI diff --git a/bin/config.d/lora-usb-umesh-1268.yaml b/bin/config.d/lora-usb-umesh-1268.yaml new file mode 100644 index 000000000..637472966 --- /dev/null +++ b/bin/config.d/lora-usb-umesh-1268.yaml @@ -0,0 +1,15 @@ +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: 30 +# Reduce output power to improve EMI 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/platformio-custom.py b/bin/platformio-custom.py index 90d733ca7..b75c66624 100644 --- a/bin/platformio-custom.py +++ b/bin/platformio-custom.py @@ -58,7 +58,16 @@ def manifest_gather(source, target, env): 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", @@ -69,7 +78,9 @@ 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}")) @@ -82,6 +93,9 @@ def manifest_gather(source, target, env): "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) 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/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/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/meshtasticd.spec.rpkg b/meshtasticd.spec.rpkg index 0819d5f8d..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) diff --git a/platformio.ini b/platformio.ini index 2bb99bac8..86580c6ae 100644 --- a/platformio.ini +++ b/platformio.ini @@ -54,6 +54,7 @@ build_flags = -Wno-missing-field-initializers -DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1 -DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware -DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1 + -DMESHTASTIC_EXCLUDE_POWERMON=1 -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage #-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now #-D OLED_PL=1 @@ -113,8 +114,7 @@ lib_deps = [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 = @@ -129,7 +129,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 @@ -142,8 +142,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 diff --git a/protobufs b/protobufs index 1a63a3d0d..4b9f104a1 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 1a63a3d0d2ff5b2df97a1476fb20cc579e144842 +Subproject commit 4b9f104a18ea43b1b2091ee2b48899fe43ad8a0b 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/MessageStore.cpp b/src/MessageStore.cpp index c96645b1c..22da418f5 100644 --- a/src/MessageStore.cpp +++ b/src/MessageStore.cpp @@ -13,6 +13,11 @@ #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; @@ -102,6 +107,60 @@ 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) { @@ -131,6 +190,11 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa } addLiveMessage(sm); + +#if ENABLE_MESSAGE_PERSISTENCE + markMessageStoreUnsaved(); +#endif + return liveMessages.back(); } @@ -155,6 +219,10 @@ void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const st sm.ackStatus = AckStatus::NONE; addLiveMessage(sm); + +#if ENABLE_MESSAGE_PERSISTENCE + markMessageStoreUnsaved(); +#endif } #if ENABLE_MESSAGE_PERSISTENCE @@ -239,6 +307,10 @@ void MessageStore::saveToFlash() f.close(); #endif + + // Reset autosave state after any save + g_messageStoreHasUnsavedChanges = false; + g_lastAutoSaveMs = millis(); } void MessageStore::loadFromFlash() @@ -270,6 +342,9 @@ void MessageStore::loadFromFlash() f.close(); #endif + // Loading messages does not trigger an autosave + g_messageStoreHasUnsavedChanges = false; + g_lastAutoSaveMs = millis(); } #else @@ -290,6 +365,11 @@ void MessageStore::clearAllMessages() 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 @@ -421,6 +501,14 @@ uint16_t MessageStore::storeText(const char *src, size_t len) 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 \ No newline at end of file +#endif diff --git a/src/MessageStore.h b/src/MessageStore.h index 41eb56b66..6203d8ed0 100644 --- a/src/MessageStore.h +++ b/src/MessageStore.h @@ -125,6 +125,11 @@ class MessageStore 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; diff --git a/src/Power.cpp b/src/Power.cpp index bb1b6ca8f..83b19ba62 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -476,7 +476,9 @@ 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); @@ -693,6 +695,8 @@ bool Power::setup() found = true; } else if (lipoChargerInit()) { found = true; + } else if (serialBatteryInit()) { + found = true; } else if (meshSolarInit()) { found = true; } else if (analogInit()) { @@ -1613,3 +1617,135 @@ 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 895dcb147..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() { 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/configuration.h b/src/configuration.h index ec1b9acc2..eb258651c 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -172,11 +172,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 +206,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 +215,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 @@ -480,6 +481,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 83a455de7..4795d2abc 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -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 3a79d97c5..dffcd8fb6 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -35,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 2be9212cf..c6ef34846 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -63,6 +63,10 @@ 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) { @@ -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; @@ -382,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 { @@ -412,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 @@ -442,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); 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 907df607c..889a5999f 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -896,14 +896,11 @@ 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); 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/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 94e252b4b..f8d6e8f7b 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -825,7 +825,7 @@ int32_t Screen::runOnce() #endif } #endif - if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { + if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0 && !suppressRebootBanner) { showSimpleBanner("Rebooting...", 0); } diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 9dbcefe73..6a69dbff7 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -438,7 +438,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, if (currentResolution == ScreenResolution::UltraLow) { 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); + snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz (%d)", freqStr, config.lora.channel_num); } } size_t len = strlen(frequencyslot); diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 7c17c8b92..c5a4106e7 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -59,17 +59,18 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp } // 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 @@ -77,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; } @@ -247,6 +250,113 @@ void menuHandler::DeviceRolePicker() screen->showOverlayBanner(bannerOptions); } +void menuHandler::FrequencySlotPicker() +{ + + 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.bandwidth; + if (loraConfig.use_preset) { + switch (loraConfig.modem_preset) { + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: + bw = (myRegion->wideLora) ? 1625.0 : 500; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: + bw = (myRegion->wideLora) ? 812.5 : 250; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: + bw = (myRegion->wideLora) ? 812.5 : 250; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: + bw = (myRegion->wideLora) ? 812.5 : 250; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: + bw = (myRegion->wideLora) ? 812.5 : 250; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: + bw = (myRegion->wideLora) ? 1625.0 : 500; + break; + default: + bw = (myRegion->wideLora) ? 812.5 : 250; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: + bw = (myRegion->wideLora) ? 406.25 : 125; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: + bw = (myRegion->wideLora) ? 406.25 : 125; + break; + } + } else { + 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; + } + + 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 = "Frequency Slot"; + bannerOptions.optionsArrayPtr = optionsArray; + 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; + } + + 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[] = { @@ -277,6 +387,8 @@ void menuHandler::RadioPresetPicker() } 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); }); @@ -449,7 +561,7 @@ void menuHandler::clockMenu() } void menuHandler::messageResponseMenu() { - enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, MuteChannel, Aloud, enumEnd }; + enum optionsNumbers { Back = 0, ViewMode, DeleteMenu, ReplyMenu, MuteChannel, Aloud, enumEnd }; static const char *optionsArray[enumEnd]; static int optionsEnumArray[enumEnd]; @@ -479,7 +591,7 @@ void menuHandler::messageResponseMenu() // Delete submenu optionsArray[options] = "Delete"; - optionsEnumArray[options++] = 900; + optionsEnumArray[options++] = DeleteMenu; #ifdef HAS_I2S optionsArray[options] = "Read Aloud"; @@ -520,34 +632,10 @@ void menuHandler::messageResponseMenu() nodeDB->saveToDisk(); } - // Delete submenu - } else if (selected == 900) { + } else if (selected == DeleteMenu) { menuHandler::menuQueue = menuHandler::delete_messages_menu; screen->runNow(); - // Delete oldest FIRST (only change) - } else if (selected == DeleteOldest) { - auto mode = graphics::MessageRenderer::getThreadMode(); - int ch = graphics::MessageRenderer::getThreadChannel(); - uint32_t peer = graphics::MessageRenderer::getThreadPeer(); - - if (mode == graphics::MessageRenderer::ThreadMode::ALL) { - // Global oldest - messageStore.deleteOldestMessage(); - } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { - // Oldest in current channel - messageStore.deleteOldestMessageInChannel(ch); - } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - // Oldest in current DM - messageStore.deleteOldestMessageWithPeer(peer); - } - - // Delete all messages - } else if (selected == DeleteAll) { - messageStore.clearAllMessages(); - graphics::MessageRenderer::clearThreadRegistries(); - graphics::MessageRenderer::clearMessageCache(); - #ifdef HAS_I2S } else if (selected == Aloud) { const meshtastic_MeshPacket &mp = devicestate.rx_text_message; @@ -716,7 +804,6 @@ void menuHandler::deleteMessagesMenu() } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { messageStore.deleteOldestMessageWithPeer(peer); } - return; } @@ -729,7 +816,6 @@ void menuHandler::deleteMessagesMenu() } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { messageStore.deleteAllMessagesWithPeer(peer); } - return; } }; @@ -1239,20 +1325,13 @@ void menuHandler::positionBaseMenu() void menuHandler::nodeListMenu() { - enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; + 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] = "Add Favorite"; - optionsEnumArray[options++] = Favorite; - optionsArray[options] = "Trace Route"; - optionsEnumArray[options++] = TraceRoute; - - if (currentResolution != ScreenResolution::UltraLow) { - optionsArray[options] = "Key Verification"; - optionsEnumArray[options++] = Verify; - } + optionsArray[options] = "Node Actions / Settings"; + optionsEnumArray[options++] = NodePicker; if (currentResolution != ScreenResolution::UltraLow) { optionsArray[options] = "Show Long/Short Name"; @@ -1267,18 +1346,12 @@ void menuHandler::nodeListMenu() 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; - screen->runNow(); } else if (selected == NodeNameLength) { menuHandler::menuQueue = menuHandler::node_name_length_menu; screen->runNow(); @@ -1287,6 +1360,159 @@ void menuHandler::nodeListMenu() 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() { static const NodeNameOption nodeNameOptions[] = { @@ -1315,6 +1541,7 @@ void menuHandler::nodeNameLengthMenu() } config.display.use_long_node_name = option.value; + saveUIConfig(); LOG_INFO("Setting names to %s", option.value ? "long" : "short"); }); @@ -1984,21 +2211,6 @@ void menuHandler::shutdownMenu() screen->showOverlayBanner(bannerOptions); } -void menuHandler::addFavoriteMenu() -{ - const char *NODE_PICKER_TITLE; - if (currentResolution == ScreenResolution::UltraLow) { - NODE_PICKER_TITLE = "Node Favorite"; - } else { - NODE_PICKER_TITLE = "Node To Favorite"; - } - screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void { - LOG_WARN("Nodenum: %u", nodenum); - nodeDB->set_favorite(true, nodenum); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - }); -} - void menuHandler::removeFavoriteMenu() { @@ -2273,7 +2485,8 @@ void menuHandler::FrameToggles_menu() lora, clock, show_favorites, - show_telemetry, + show_env_telemetry, + show_aq_telemetry, show_power, enumEnd }; @@ -2318,8 +2531,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; @@ -2382,10 +2598,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; @@ -2442,6 +2662,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; @@ -2510,8 +2733,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(); diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 445513e25..45fd0bf5f 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -13,6 +13,7 @@ class menuHandler lora_picker, device_role_picker, radio_preset_picker, + frequency_slot, no_timeout_lora_picker, TZ_picker, twelve_hour_picker, @@ -33,7 +34,8 @@ class menuHandler brightness_picker, reboot_menu, shutdown_menu, - add_favorite, + NodePicker_menu, + Manage_Node_menu, remove_favorite, test_menu, number_test, @@ -55,12 +57,14 @@ class menuHandler 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(); @@ -90,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(); @@ -149,6 +155,7 @@ using GPSToggleOption = MenuOption; using GPSFormatOption = MenuOption; using NodeNameOption = MenuOption; using PositionMenuOption = MenuOption; +using ManageNodeOption = MenuOption; using ClockFaceOption = MenuOption; } // namespace graphics diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index e10d8c40a..9d6780130 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -176,6 +176,7 @@ int calculateMaxScroll(int totalEntries, int visibleRows) void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { + x = (currentResolution == ScreenResolution::High) ? x - 2 : (currentResolution == ScreenResolution::Low) ? x - 1 : x; for (int y = yStart; y <= yEnd; y += 2) { display->setPixel(x, y); } @@ -205,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 nameMaxWidth = columnWidth - 25; int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); 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); @@ -234,6 +237,13 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int 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 @@ -253,6 +263,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int int barsXOffset = columnWidth - barsOffset; 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); @@ -265,6 +276,13 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int 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; @@ -298,6 +316,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); 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()); @@ -358,6 +377,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 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 = (currentResolution == ScreenResolution::High) @@ -392,6 +418,7 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); 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); @@ -403,6 +430,13 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 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, 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/main.cpp b/src/main.cpp index 786c3dcf4..15ab12255 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -38,6 +38,9 @@ #include "target_specific.h" #include #include +#if HAS_SCREEN +#include "MessageStore.h" +#endif #ifdef ELECROW_ThinkNode_M5 PCA9557 io(0x18, &Wire); @@ -592,6 +595,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 != "") { @@ -776,11 +780,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); @@ -1559,8 +1564,9 @@ 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. @@ -1673,6 +1679,9 @@ void loop() if (dispdev) static_cast(dispdev)->sdlLoop(); } +#endif +#if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE + messageStoreAutosaveTick(); #endif long delayMsec = mainController.runOrDelay(); diff --git a/src/main.h b/src/main.h index 7ca14d825..c3528a63d 100644 --- a/src/main.h +++ b/src/main.h @@ -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/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index af6dd92e9..341afe78d 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -186,7 +186,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/NodeDB.cpp b/src/mesh/NodeDB.cpp index 8913e0019..375bc76e3 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -53,7 +53,7 @@ #endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI -#include +#include #endif NodeDB *nodeDB = nullptr; @@ -756,8 +756,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 @@ -823,7 +823,7 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.nag_timeout = 2; #endif #if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || \ - defined(ELECROW_ThinkNode_M6) + defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M6) // Default to PIN_LED2 for external notification output (LED color depends on device variant) moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.output = PIN_LED2; @@ -1264,6 +1264,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); } diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 817e31617..adf2b42ea 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -378,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..5588fc348 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -193,7 +193,7 @@ bool RF95Interface::init() return res == RADIOLIB_ERR_NONE; } -void INTERRUPT_ATTR RF95Interface::disableInterrupt() +void RF95Interface::disableInterrupt() { lora->clearDio0Action(); } diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 0e3069c14..498496a3b 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -256,7 +256,7 @@ template bool SX126xInterface::reconfigure() return RADIOLIB_ERR_NONE; } -template void INTERRUPT_ATTR SX126xInterface::disableInterrupt() +template void SX126xInterface::disableInterrupt() { lora.clearDio1Action(); } diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 80872af07..b4278c636 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -155,7 +155,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.h b/src/mesh/generated/meshtastic/admin.pb.h index 047ef2c14..26b4343e9 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -281,8 +281,6 @@ typedef struct _meshtastic_AdminMessage { meshtastic_SharedContact add_contact; /* Initiate or respond to a key verification request */ meshtastic_KeyVerificationAdmin key_verification; - /* Tell the node to reboot into OTA mode for firmware update via BLE or WiFi (ESP32 only for now) */ - meshtastic_OTAMode reboot_ota_mode; /* 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) @@ -341,7 +339,6 @@ extern "C" { #define meshtastic_AdminMessage_payload_variant_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation #define meshtastic_AdminMessage_payload_variant_restore_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation #define meshtastic_AdminMessage_payload_variant_remove_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation -#define meshtastic_AdminMessage_payload_variant_reboot_ota_mode_ENUMTYPE meshtastic_OTAMode #define meshtastic_AdminMessage_OTAEvent_reboot_ota_mode_ENUMTYPE meshtastic_OTAMode @@ -436,7 +433,6 @@ extern "C" { #define meshtastic_AdminMessage_commit_edit_settings_tag 65 #define meshtastic_AdminMessage_add_contact_tag 66 #define meshtastic_AdminMessage_key_verification_tag 67 -#define meshtastic_AdminMessage_reboot_ota_mode_tag 68 #define meshtastic_AdminMessage_factory_reset_device_tag 94 #define meshtastic_AdminMessage_reboot_ota_seconds_tag 95 #define meshtastic_AdminMessage_exit_simulator_tag 96 @@ -497,7 +493,6 @@ X(a, STATIC, ONEOF, BOOL, (payload_variant,begin_edit_settings,begin_ed X(a, STATIC, ONEOF, BOOL, (payload_variant,commit_edit_settings,commit_edit_settings), 65) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,add_contact,add_contact), 66) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification,key_verification), 67) \ -X(a, STATIC, ONEOF, UENUM, (payload_variant,reboot_ota_mode,reboot_ota_mode), 68) \ X(a, STATIC, ONEOF, INT32, (payload_variant,factory_reset_device,factory_reset_device), 94) \ X(a, STATIC, ONEOF, INT32, (payload_variant,reboot_ota_seconds,reboot_ota_seconds), 95) \ X(a, STATIC, ONEOF, BOOL, (payload_variant,exit_simulator,exit_simulator), 96) \ diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index d4ef5bee4..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. */ diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 58401143c..68552ede5 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -296,6 +296,10 @@ typedef enum _meshtastic_HardwareModel { 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. ------------------------------------------------------------------------------------------------------------------------------------------ */ @@ -1366,10 +1370,6 @@ extern "C" { #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_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 #define _meshtastic_MeshPacket_Priority_ARRAYSIZE ((meshtastic_MeshPacket_Priority)(meshtastic_MeshPacket_Priority_MAX+1)) diff --git a/src/mesh/generated/meshtastic/module_config.pb.h b/src/mesh/generated/meshtastic/module_config.pb.h index 2b7b54949..dd0151e3f 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 */ @@ -483,8 +486,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 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 5f0c27fff..990ca0f46 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,27 @@ 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()) { - if (screen) - screen->startFirmwareUpdateScreen(); - BleOta::switchToOtaApp(); - LOG_INFO("Rebooting to BLE OTA"); + if (r->ota_request.ota_hash.size != 32) { + suppressRebootBanner = true; + LOG_INFO("OTA Failed: Invalid `ota_hash` provided"); + break; } -#endif -#if !MESHTASTIC_EXCLUDE_WIFI - if (WiFiOTA::trySwitchToOTA()) { + + meshtastic_OTAMode mode = r->ota_request.reboot_ota_mode; + if (MeshtasticOTA::trySwitchToOTA()) { + LOG_INFO("OTA Requested"); + suppressRebootBanner = true; if (screen) screen->startFirmwareUpdateScreen(); - WiFiOTA::saveConfig(&config.network); + MeshtasticOTA::saveConfig(&config.network, mode, r->ota_request.ota_hash.bytes); LOG_INFO("Rebooting to WiFi OTA"); + } else { + LOG_INFO("WIFI OTA Failed"); } #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 +381,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()); diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index 3f6375a65..8b7ce700a 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -459,7 +459,16 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP } } + meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from); meshtastic_Channel ch = channels.getByIndex(mp.channel ? mp.channel : channels.getPrimaryIndex()); + + // 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 directToUs = !isBroadcast(mp.to) && isToUs(&mp); + bool is_muted = directToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0)) + : (ch.settings.has_module_settings && ch.settings.module_settings.is_muted); + if (moduleConfig.external_notification.alert_bell) { if (containsBell) { LOG_INFO("externalNotificationModule - Notification Bell"); @@ -510,8 +519,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP } } - if (moduleConfig.external_notification.alert_message && - (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { + if (moduleConfig.external_notification.alert_message && !is_muted) { LOG_INFO("externalNotificationModule - Notification Module"); isNagging = true; setExternalState(0, true); @@ -522,8 +530,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP } } - if (moduleConfig.external_notification.alert_message_vibra && - (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { + if (moduleConfig.external_notification.alert_message_vibra && !is_muted) { LOG_INFO("externalNotificationModule - Notification Module (Vibra)"); isNagging = true; setExternalState(1, true); @@ -534,8 +541,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP } } - if (moduleConfig.external_notification.alert_message_buzzer && - (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { + if (moduleConfig.external_notification.alert_message_buzzer && !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))) { diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 63392f7e4..e17868baf 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -252,9 +252,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 diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index f6007a565..5699f3be6 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -64,8 +64,9 @@ SerialModule *serialModule; SerialModuleRadio *serialModuleRadio; #if defined(TTGO_T_ECHO) || defined(TTGO_T_ECHO_PLUS) || 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) + defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M4) || 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; @@ -205,8 +206,9 @@ int32_t SerialModule::runOnce() Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); } #elif !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \ - !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && \ - !defined(MUZI_BASE) + !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \ + !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) + if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { #ifdef ARCH_RP2040 Serial2.setFIFOSize(RX_BUFFER); @@ -263,7 +265,8 @@ int32_t SerialModule::runOnce() } #if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \ - !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) + !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \ + !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { processWXSerial(); @@ -539,7 +542,10 @@ void SerialModule::processWXSerial() { #if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !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) + !defined(ELECROW_ThinkNode_M3) && \ + !defined(ELECROW_ThinkNode_M4) && \ + !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) && !defined(MUZI_BASE) + 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 8738c16ca..33aa58127 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) @@ -60,6 +62,12 @@ 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; @@ -103,6 +111,21 @@ 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 @@ -111,5 +134,18 @@ int32_t StatusLEDModule::runOnce() 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 d90ff718c..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,12 +28,15 @@ 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; 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 843d7b8d5..ec6fe4799 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -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) { @@ -642,8 +615,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/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/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/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/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 ProtobufModuleisScreenOn() && !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/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 3b98eca3d..fc1f27ea2 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 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 77% rename from src/platform/esp32/WiFiOTA.cpp rename to src/platform/esp32/MeshtasticOTA.cpp index 4cf157b4c..b8cb052ef 100644 --- a/src/platform/esp32/WiFiOTA.cpp +++ b/src/platform/esp32/MeshtasticOTA.cpp @@ -1,13 +1,13 @@ -#include "WiFiOTA.h" +#include "MeshtasticOTA.h" #include "configuration.h" #include #include -namespace WiFiOTA +namespace MeshtasticOTA { -static const char *nvsNamespace = "ota-wifi"; -static const char *appProjectName = "OTA-WiFi"; +static const char *nvsNamespace = "MeshtasticOTA"; +static const char *appProjectName = "MeshtasticOTA"; static bool updated = false; @@ -43,12 +43,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,10 +64,14 @@ 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) + if (esp_ota_get_partition_description(part, app_desc) != ESP_OK) { + LOG_INFO("esp_ota_get_partition_description failed"); return false; - if (strcmp(app_desc->project_name, appProjectName) != 0) + } + if (strcmp(app_desc->project_name, appProjectName) != 0) { + LOG_INFO("app_desc->project_name == 0"); return false; + } return true; } @@ -89,4 +95,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..001eba039 --- /dev/null +++ b/src/platform/esp32/MeshtasticOTA.h @@ -0,0 +1,18 @@ +#ifndef MESHTASTICOTA_H +#define MESHTASTICOTA_H + +#include "mesh-pb-constants.h" +#include + +namespace MeshtasticOTA +{ +void initialize(); +bool isUpdated(); + +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 88dfacf92..11c9c6ff0 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -195,6 +195,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/nrf52/architecture.h b/src/platform/nrf52/architecture.h index afe96963d..7734c0020 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -74,6 +74,8 @@ #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/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index af7e275c6..ec9bbedca 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -55,6 +55,7 @@ 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) { @@ -63,6 +64,7 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state) if (sscanf(arg, "%d", &TCPPort) < 1) return ARGP_ERR_UNKNOWN; else + checkConfigPort = false; printf("Using config file %d\n", TCPPort); break; case 'c': @@ -487,6 +489,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 @@ -504,6 +511,19 @@ 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") { @@ -717,6 +737,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(""); } @@ -842,6 +872,14 @@ 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; diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index 9335be90a..3a6887421 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -2,6 +2,7 @@ #include #include #include +#include #include "LR11x0Interface.h" #include "Module.h" @@ -97,6 +98,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; @@ -173,6 +175,7 @@ extern struct portduino_config_struct { 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; @@ -300,6 +303,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; @@ -492,6 +509,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 != "") @@ -503,4 +522,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/power.h b/src/power.h index c826d98b4..5f887c36b 100644 --- a/src/power.h +++ b/src/power.h @@ -121,6 +121,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/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini index e582b6880..3ee2b9516 100644 --- a/variants/esp32/esp32-common.ini +++ b/variants/esp32/esp32-common.ini @@ -49,6 +49,7 @@ build_flags = -DLIBPAX_BLE -DHAS_UDP_MULTICAST=1 ;-DDEBUG_HEAP + -DCAN_RECLOCK_I2C lib_deps = ${arduino_base.lib_deps} @@ -65,8 +66,6 @@ lib_deps = 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 # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 diff --git a/variants/esp32/esp32.ini b/variants/esp32/esp32.ini index d1a8a63b0..5999bc098 100644 --- a/variants/esp32/esp32.ini +++ b/variants/esp32/esp32.ini @@ -5,4 +5,5 @@ extends = esp32_common custom_esp32_kind = esp32 build_flags = - ${esp32_common.build_flags} \ No newline at end of file + ${esp32_common.build_flags} + -DMESHTASTIC_EXCLUDE_AUDIO=1 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/esp32c3/esp32c3.ini b/variants/esp32c3/esp32c3.ini index 2d7ae71bc..e5f117ad7 100644 --- a/variants/esp32c3/esp32c3.ini +++ b/variants/esp32c3/esp32c3.ini @@ -4,3 +4,8 @@ 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/esp32s2/esp32s2.ini b/variants/esp32s2/esp32s2.ini index c806943ee..836e31d8d 100644 --- a/variants/esp32s2/esp32s2.ini +++ b/variants/esp32s2/esp32s2.ini @@ -12,7 +12,12 @@ 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_common.lib_ignore} NimBLE-Arduino 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..1448b1d74 --- /dev/null +++ b/variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h @@ -0,0 +1,19 @@ +#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/ELECROW-ThinkNode-M5/platformio.ini b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini index 59ac625b6..9994cf665 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini @@ -27,6 +27,6 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip + 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/crowpanel-esp32s3-5-epaper/platformio.ini b/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini index 315a53ffd..7a0bd31b4 100644 --- a/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini +++ b/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini @@ -26,7 +26,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:crowpanel-esp32s3-4-epaper] extends = esp32s3_base @@ -56,7 +56,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:crowpanel-esp32s3-2-epaper] extends = esp32s3_base @@ -86,4 +86,4 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip diff --git a/variants/esp32s3/elecrow_panel/platformio.ini b/variants/esp32s3/elecrow_panel/platformio.ini index 5c9a4bfaf..e0f6f0760 100644 --- a/variants/esp32s3/elecrow_panel/platformio.ini +++ b/variants/esp32s3/elecrow_panel/platformio.ini @@ -44,11 +44,10 @@ lib_deps = ${esp32s3_base.lib_deps} # 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.0.1 - # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.0 ; note: v1.2.7 breaks the elecrow 7" display functionality + 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 diff --git a/variants/esp32s3/esp32s3.ini b/variants/esp32s3/esp32s3.ini index 5e333f3ce..299415442 100644 --- a/variants/esp32s3/esp32s3.ini +++ b/variants/esp32s3/esp32s3.ini @@ -3,3 +3,8 @@ 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/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini index 6582335af..4495a409f 100644 --- a/variants/esp32s3/heltec_v4/platformio.ini +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -34,6 +34,17 @@ build_flags = -D I2C_SCL1=3 [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 diff --git a/variants/esp32s3/heltec_vision_master_e213/platformio.ini b/variants/esp32s3/heltec_vision_master_e213/platformio.ini index a03755970..4ace5a45a 100644 --- a/variants/esp32s3/heltec_vision_master_e213/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_e213/platformio.ini @@ -29,7 +29,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip upload_speed = 115200 [env:heltec-vision-master-e213-inkhud] diff --git a/variants/esp32s3/heltec_vision_master_e290/platformio.ini b/variants/esp32s3/heltec_vision_master_e290/platformio.ini index 4cc913668..e86746b67 100644 --- a/variants/esp32s3/heltec_vision_master_e290/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_e290/platformio.ini @@ -32,7 +32,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/448c8538129fde3d02a7cb5e6fc81971ad92547f.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip upload_speed = 115200 [env:heltec-vision-master-e290-inkhud] diff --git a/variants/esp32s3/heltec_wireless_paper/platformio.ini b/variants/esp32s3/heltec_wireless_paper/platformio.ini index ac32fb219..673c834ea 100644 --- a/variants/esp32s3/heltec_wireless_paper/platformio.ini +++ b/variants/esp32s3/heltec_wireless_paper/platformio.ini @@ -29,7 +29,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip upload_speed = 115200 [env:heltec-wireless-paper-inkhud] diff --git a/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini b/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini index a4a21c55c..8543e414f 100644 --- a/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini +++ b/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini @@ -26,5 +26,5 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip upload_speed = 115200 diff --git a/variants/esp32s3/t-beam-1w/platformio.ini b/variants/esp32s3/t-beam-1w/platformio.ini index 54ddb6c3e..9abf895db 100644 --- a/variants/esp32s3/t-beam-1w/platformio.ini +++ b/variants/esp32s3/t-beam-1w/platformio.ini @@ -1,5 +1,14 @@ ; 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 diff --git a/variants/esp32s3/t-deck-pro/platformio.ini b/variants/esp32s3/t-deck-pro/platformio.ini index 28fef86ba..b2c91dcf5 100644 --- a/variants/esp32s3/t-deck-pro/platformio.ini +++ b/variants/esp32s3/t-deck-pro/platformio.ini @@ -29,7 +29,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.4 + zinggjm/GxEPD2@1.6.5 # 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 diff --git a/variants/esp32s3/t-watch-s3/variant.h b/variants/esp32s3/t-watch-s3/variant.h index dfd219391..216dda589 100644 --- a/variants/esp32s3/t-watch-s3/variant.h +++ b/variants/esp32s3/t-watch-s3/variant.h @@ -53,10 +53,13 @@ #define HAS_BMA423 1 #define BMA4XX_INT 14 // Interrupt for BMA_423 axis sensor +#define HAS_GPS 1 #define GPS_DEFAULT_NOT_PRESENT 1 #define GPS_BAUDRATE 38400 -#define GPS_RX_PIN 42 -#define GPS_TX_PIN 41 +#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/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini index 3a7afb016..5973db1d0 100644 --- a/variants/esp32s3/tlora-pager/platformio.ini +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -33,7 +33,7 @@ lib_deps = ${esp32s3_base.lib_deps} # 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.0.1 + 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 @@ -41,7 +41,7 @@ lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.3 # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver - https://github.com/pschatzmann/arduino-audio-driver/archive/v0.1.3.zip + https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.0.zip # TODO renovate https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip # TODO renovate diff --git a/variants/esp32s3/tlora_t3s3_epaper/platformio.ini b/variants/esp32s3/tlora_t3s3_epaper/platformio.ini index fdf3f7814..256cdc0d0 100644 --- a/variants/esp32s3/tlora_t3s3_epaper/platformio.ini +++ b/variants/esp32s3/tlora_t3s3_epaper/platformio.ini @@ -31,7 +31,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:tlora-t3s3-epaper-inkhud] extends = esp32s3_base, inkhud diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini index 041d3b76f..a4687669b 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini @@ -33,7 +33,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW 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/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip + 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 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..af9bed998 --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp @@ -0,0 +1,51 @@ +/* + 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(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + 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); +} diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h new file mode 100644 index 000000000..faca5b075 --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h @@ -0,0 +1,142 @@ +/* + 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_BUILTIN -1 +#define LED_BLUE -1 +#define PIN_LED2 (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 + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h index 984f967d8..e46391207 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h @@ -134,12 +134,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/heltec_mesh_pocket/platformio.ini b/variants/nrf52840/heltec_mesh_pocket/platformio.ini index 4dc8b78e7..646304a5a 100644 --- a/variants/nrf52840/heltec_mesh_pocket/platformio.ini +++ b/variants/nrf52840/heltec_mesh_pocket/platformio.ini @@ -38,7 +38,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_ 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/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:heltec-mesh-pocket-5000-inkhud] extends = nrf52840_base, inkhud @@ -101,7 +101,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_ 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/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:heltec-mesh-pocket-10000-inkhud] extends = nrf52840_base, inkhud diff --git a/variants/nrf52840/heltec_mesh_solar/platformio.ini b/variants/nrf52840/heltec_mesh_solar/platformio.ini index 2ad699544..69264f0df 100644 --- a/variants/nrf52840/heltec_mesh_solar/platformio.ini +++ b/variants/nrf52840/heltec_mesh_solar/platformio.ini @@ -16,7 +16,7 @@ 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 # renovate: datasource=custom.pio depName=ArduinoJson packageName=bblanchon/library/ArduinoJson - bblanchon/ArduinoJson@6.21.4 + bblanchon/ArduinoJson@6.21.5 [env:heltec-mesh-solar] custom_meshtastic_hw_model = 108 diff --git a/variants/nrf52840/meshlink/platformio.ini b/variants/nrf52840/meshlink/platformio.ini index 26a999fbb..e2631affe 100644 --- a/variants/nrf52840/meshlink/platformio.ini +++ b/variants/nrf52840/meshlink/platformio.ini @@ -47,7 +47,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshlin 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/55f618961db45a23eff0233546430f1e5a80f63a.zip + 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/rak4631_eth_gw/platformio.ini b/variants/nrf52840/rak4631_eth_gw/platformio.ini index bfa4924ce..e06a271aa 100644 --- a/variants/nrf52840/rak4631_eth_gw/platformio.ini +++ b/variants/nrf52840/rak4631_eth_gw/platformio.ini @@ -36,7 +36,7 @@ lib_deps = # 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=ArduinoJson packageName=bblanchon/library/ArduinoJson - bblanchon/ArduinoJson@6.21.4 + 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 diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini index 6b36a79c3..60d83b95a 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini @@ -34,7 +34,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_w 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/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip debug_tool = jlink [env:seeed_wio_tracker_L1_eink-inkhud] diff --git a/variants/nrf52840/t-echo/platformio.ini b/variants/nrf52840/t-echo/platformio.ini index a8fc027c8..9a66890a7 100644 --- a/variants/nrf52840/t-echo/platformio.ini +++ b/variants/nrf52840/t-echo/platformio.ini @@ -30,7 +30,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo> 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/55f618961db45a23eff0233546430f1e5a80f63a.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.3 ;upload_protocol = fs 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/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