From c86ae584abae33d9668c202aeadec23346d3ff67 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 7 Oct 2026 19:04:39 +0300 Subject: [PATCH] refactor: rebrand to PosadMesh, translate docs to Russian, drop legacy brokers --- .../setup-build-environment/action.yml | 42 +- ...astmesh-build-companion-wifi-firmwares.yml | 42 - ...stmesh-build-observer-espnow-firmwares.yml | 41 - .../eastmesh-build-observer-firmwares.yml | 41 - ...h-build-observer-mqtt-bridge-firmwares.yml | 41 - ...build-repeater-bridge-espnow-firmwares.yml | 42 - .github/workflows/eastmesh-github-pages.yml | 42 - .github/workflows/eastmesh-pr-build-check.yml | 39 - .../workflows/eastmesh-push-build-check.yml | 39 - AGENTS.md | 257 +- README.md | 304 +- RELEASE.md | 123 +- arch/esp32/CPUUsageTracker.cpp | 48 + arch/esp32/CPUUsageTracker.h | 40 + .../extra_scripts/generate_web_panel_cert.py | 2 +- arch/esp32/extra_scripts/git_commit_hash.py | 18 + arch/esp32/task_pinning.c | 67 + arch/esp32/tls_server_config.c | 115 + boards/diy_esp32s3_n16r8_e22_back2back.json | 44 + boards/diy_esp32s3_n16r8_e22p_back2back.json | 44 + eastmesh-docs/api.md | 410 --- eastmesh-docs/boards.md | 196 -- eastmesh-docs/custom-cli.md | 326 --- eastmesh-docs/index.md | 72 - eastmesh-docs/local-builds.md | 200 -- eastmesh-docs/migration.md | 121 - eastmesh-docs/releases.md | 209 -- eastmesh-docs/web-panel.md | 433 --- examples/companion_radio/ui-new/UITask.cpp | 8 +- examples/simple_repeater/MyMesh.cpp | 503 +++- examples/simple_repeater/MyMesh.h | 44 +- mkdocs.yml | 33 +- platformio.ini | 7 + eastmesh-build.sh => posadmesh-build.sh | 38 +- .../_assets/repeater_web_panel_app_dark.png | Bin .../_assets/repeater_web_panel_app_light.png | Bin .../_assets/repeater_web_panel_stats_dark.png | Bin .../repeater_web_panel_stats_light.png | Bin .../_assets/xjarid.svg | 0 .../_stylesheets/extra.css | 0 posadmesh-docs/api.md | 410 +++ posadmesh-docs/boards.md | 196 ++ posadmesh-docs/custom-cli.md | 313 ++ posadmesh-docs/index.md | 72 + posadmesh-docs/local-builds.md | 200 ++ posadmesh-docs/migration.md | 121 + posadmesh-docs/releases.md | 209 ++ posadmesh-docs/web-panel.md | 433 +++ .../web-heap-check.sh | 4 +- pyproject.toml | 4 +- release-notes.yml | 135 +- src/helpers/ESP32Board.h | 5 + src/helpers/MeshadventurerBoard.h | 30 + src/helpers/NetworkPrefs.cpp | 6 +- src/helpers/NetworkPrefs.h | 1 + src/helpers/NetworkService.cpp | 1 + src/helpers/NetworkService.h | 6 +- src/helpers/StatsHistory.cpp | 116 +- src/helpers/StatsHistory.h | 4 +- src/helpers/mqtt/MQTTCaCerts.h | 113 - src/helpers/mqtt/MQTTPrefs.cpp | 10 +- src/helpers/mqtt/MQTTUplink.cpp | 493 +--- src/helpers/mqtt/MQTTUplink.h | 36 +- src/helpers/ui/DisplayDriver.h | 63 + src/helpers/ui/SSD1306Display.cpp | 18 +- src/helpers/ui/SSD1306Display.h | 3 + src/helpers/ui/glcdfont6x8.h | 460 +++ src/helpers/web/EastMeshFavicon.h | 153 - src/helpers/web/WebCertStore.cpp | 467 +++ src/helpers/web/WebCertStore.h | 84 + src/helpers/web/WebPanelServer.cpp | 2607 +++++++++-------- src/helpers/web/WebPanelServer.h | 25 +- src/helpers/web/WebService.cpp | 80 +- src/helpers/web/WebService.h | 15 +- uv.lock | 2 +- .../DIYESP32S3N16R8E22Back2backBoard.h | 54 + .../platformio.ini | 234 ++ .../target.cpp | 57 + .../diy_esp32s3_n16r8_e22_back2back/target.h | 21 + variants/meshadventurer/platformio.ini | 1 + .../platformio.ini | 767 ++--- 81 files changed, 6942 insertions(+), 5118 deletions(-) delete mode 100644 .github/workflows/eastmesh-build-companion-wifi-firmwares.yml delete mode 100644 .github/workflows/eastmesh-build-observer-espnow-firmwares.yml delete mode 100644 .github/workflows/eastmesh-build-observer-firmwares.yml delete mode 100644 .github/workflows/eastmesh-build-observer-mqtt-bridge-firmwares.yml delete mode 100644 .github/workflows/eastmesh-build-repeater-bridge-espnow-firmwares.yml delete mode 100644 .github/workflows/eastmesh-github-pages.yml delete mode 100644 .github/workflows/eastmesh-pr-build-check.yml delete mode 100644 .github/workflows/eastmesh-push-build-check.yml create mode 100644 arch/esp32/CPUUsageTracker.cpp create mode 100644 arch/esp32/CPUUsageTracker.h create mode 100644 arch/esp32/extra_scripts/git_commit_hash.py create mode 100644 arch/esp32/task_pinning.c create mode 100644 arch/esp32/tls_server_config.c create mode 100644 boards/diy_esp32s3_n16r8_e22_back2back.json create mode 100644 boards/diy_esp32s3_n16r8_e22p_back2back.json delete mode 100644 eastmesh-docs/api.md delete mode 100644 eastmesh-docs/boards.md delete mode 100644 eastmesh-docs/custom-cli.md delete mode 100644 eastmesh-docs/index.md delete mode 100644 eastmesh-docs/local-builds.md delete mode 100644 eastmesh-docs/migration.md delete mode 100644 eastmesh-docs/releases.md delete mode 100644 eastmesh-docs/web-panel.md rename eastmesh-build.sh => posadmesh-build.sh (91%) rename {eastmesh-docs => posadmesh-docs}/_assets/repeater_web_panel_app_dark.png (100%) rename {eastmesh-docs => posadmesh-docs}/_assets/repeater_web_panel_app_light.png (100%) rename {eastmesh-docs => posadmesh-docs}/_assets/repeater_web_panel_stats_dark.png (100%) rename {eastmesh-docs => posadmesh-docs}/_assets/repeater_web_panel_stats_light.png (100%) rename {eastmesh-docs => posadmesh-docs}/_assets/xjarid.svg (100%) rename {eastmesh-docs => posadmesh-docs}/_stylesheets/extra.css (100%) create mode 100644 posadmesh-docs/api.md create mode 100644 posadmesh-docs/boards.md create mode 100644 posadmesh-docs/custom-cli.md create mode 100644 posadmesh-docs/index.md create mode 100644 posadmesh-docs/local-builds.md create mode 100644 posadmesh-docs/migration.md create mode 100644 posadmesh-docs/releases.md create mode 100644 posadmesh-docs/web-panel.md rename {eastmesh-tools => posadmesh-tools}/web-heap-check.sh (95%) delete mode 100644 src/helpers/mqtt/MQTTCaCerts.h create mode 100644 src/helpers/ui/glcdfont6x8.h delete mode 100644 src/helpers/web/EastMeshFavicon.h create mode 100644 src/helpers/web/WebCertStore.cpp create mode 100644 src/helpers/web/WebCertStore.h create mode 100644 variants/diy_esp32s3_n16r8_e22_back2back/DIYESP32S3N16R8E22Back2backBoard.h create mode 100644 variants/diy_esp32s3_n16r8_e22_back2back/platformio.ini create mode 100644 variants/diy_esp32s3_n16r8_e22_back2back/target.cpp create mode 100644 variants/diy_esp32s3_n16r8_e22_back2back/target.h rename variants/{eastmesh => posadmesh_mqtt}/platformio.ini (61%) diff --git a/.github/actions/setup-build-environment/action.yml b/.github/actions/setup-build-environment/action.yml index 673774e7..0ef576f5 100644 --- a/.github/actions/setup-build-environment/action.yml +++ b/.github/actions/setup-build-environment/action.yml @@ -43,18 +43,18 @@ runs: # Active release tag formats: # companion-wifi-v1.15.0 # repeater-bridge-espnow-v1.15.0 - # observer-eastmesh-bridge-espnow-v2026.5.1 - # observer-eastmesh-bridge-mqtt-v2026.7.0 - # observer-eastmesh-v2026.5.1 + # observer-posadmesh-bridge-espnow-v2026.5.1 + # observer-posadmesh-bridge-mqtt-v2026.7.0 + # observer-posadmesh-v2026.5.1 # with OFFICIAL_MESHCORE_VERSION=v1.15.0 configured as a GitHub variable. # # This exports: # companion-wifi-v1.15.0 -> FIRMWARE_VERSION=v1.15.0, RELEASE_VERSION=v1.15.0 # repeater-bridge-espnow-v1.15.0 -> FIRMWARE_VERSION=v1.15.0, RELEASE_VERSION=v1.15.0 - # observer-eastmesh-bridge-espnow-v2026.5.1 -> FIRMWARE_VERSION=v1.15.0, EASTMESH_VERSION=v2026.5.1, - # RELEASE_VERSION=v1.15.0-eastmesh-v2026.5.1 - # observer-eastmesh-v2026.5.1 -> FIRMWARE_VERSION=v1.15.0, EASTMESH_VERSION=v2026.5.1, - # RELEASE_VERSION=v1.15.0-eastmesh-v2026.5.1 + # observer-posadmesh-bridge-espnow-v2026.5.1 -> FIRMWARE_VERSION=v1.15.0, POSADMESH_VERSION=v2026.5.1, + # RELEASE_VERSION=v1.15.0-posadmesh-v2026.5.1 + # observer-posadmesh-v2026.5.1 -> FIRMWARE_VERSION=v1.15.0, POSADMESH_VERSION=v2026.5.1, + # RELEASE_VERSION=v1.15.0-posadmesh-v2026.5.1 - name: Extract Version Info shell: bash run: | @@ -64,34 +64,34 @@ runs: echo "GIT_TAG_NAME=" >> "$GITHUB_ENV" echo "GIT_TAG_VERSION=" >> "$GITHUB_ENV" echo "OFFICIAL_MESHCORE_VERSION=$STATIC_OFFICIAL_VERSION" >> "$GITHUB_ENV" - echo "EASTMESH_VERSION=" >> "$GITHUB_ENV" + echo "POSADMESH_VERSION=" >> "$GITHUB_ENV" echo "FIRMWARE_VERSION=$STATIC_OFFICIAL_VERSION" >> "$GITHUB_ENV" echo "RELEASE_VERSION=$STATIC_OFFICIAL_VERSION" >> "$GITHUB_ENV" exit 0 fi GIT_TAG_NAME="${GITHUB_REF#refs/tags/}" - EASTMESH_VERSION="" + POSADMESH_VERSION="" FIRMWARE_VERSION="$STATIC_OFFICIAL_VERSION" - if [[ "$GIT_TAG_NAME" =~ ^observer-eastmesh-(v[[:alnum:]._-]+)$ ]]; then + if [[ "$GIT_TAG_NAME" =~ ^observer-posadmesh-(v[[:alnum:]._-]+)$ ]]; then if [[ -z "$STATIC_OFFICIAL_VERSION" ]]; then - echo "OFFICIAL_MESHCORE_VERSION must be set for EastMesh release tags" >&2 + echo "OFFICIAL_MESHCORE_VERSION must be set for PosadMesh release tags" >&2 exit 1 fi - EASTMESH_VERSION="${BASH_REMATCH[1]}" - elif [[ "$GIT_TAG_NAME" =~ ^observer-eastmesh-bridge-espnow-(v[[:alnum:]._-]+)$ ]]; then + POSADMESH_VERSION="${BASH_REMATCH[1]}" + elif [[ "$GIT_TAG_NAME" =~ ^observer-posadmesh-bridge-espnow-(v[[:alnum:]._-]+)$ ]]; then if [[ -z "$STATIC_OFFICIAL_VERSION" ]]; then - echo "OFFICIAL_MESHCORE_VERSION must be set for EastMesh release tags" >&2 + echo "OFFICIAL_MESHCORE_VERSION must be set for PosadMesh release tags" >&2 exit 1 fi - EASTMESH_VERSION="${BASH_REMATCH[1]}" - elif [[ "$GIT_TAG_NAME" =~ ^observer-eastmesh-bridge-mqtt-(v[[:alnum:]._-]+)$ ]]; then + POSADMESH_VERSION="${BASH_REMATCH[1]}" + elif [[ "$GIT_TAG_NAME" =~ ^observer-posadmesh-bridge-mqtt-(v[[:alnum:]._-]+)$ ]]; then if [[ -z "$STATIC_OFFICIAL_VERSION" ]]; then - echo "OFFICIAL_MESHCORE_VERSION must be set for EastMesh release tags" >&2 + echo "OFFICIAL_MESHCORE_VERSION must be set for PosadMesh release tags" >&2 exit 1 fi - EASTMESH_VERSION="${BASH_REMATCH[1]}" + POSADMESH_VERSION="${BASH_REMATCH[1]}" elif [[ "$GIT_TAG_NAME" =~ ^companion-wifi-(v[[:alnum:]._-]+)$ ]]; then FIRMWARE_VERSION="${BASH_REMATCH[1]}" elif [[ "$GIT_TAG_NAME" =~ ^repeater-bridge-espnow-(v[[:alnum:]._-]+)$ ]]; then @@ -107,13 +107,13 @@ runs: fi RELEASE_VERSION="$FIRMWARE_VERSION" - if [[ -n "$EASTMESH_VERSION" ]]; then - RELEASE_VERSION="${RELEASE_VERSION}-eastmesh-${EASTMESH_VERSION}" + if [[ -n "$POSADMESH_VERSION" ]]; then + RELEASE_VERSION="${RELEASE_VERSION}-posadmesh-${POSADMESH_VERSION}" fi echo "GIT_TAG_NAME=$GIT_TAG_NAME" >> "$GITHUB_ENV" echo "GIT_TAG_VERSION=$RELEASE_VERSION" >> "$GITHUB_ENV" echo "OFFICIAL_MESHCORE_VERSION=$FIRMWARE_VERSION" >> "$GITHUB_ENV" - echo "EASTMESH_VERSION=$EASTMESH_VERSION" >> "$GITHUB_ENV" + echo "POSADMESH_VERSION=$POSADMESH_VERSION" >> "$GITHUB_ENV" echo "FIRMWARE_VERSION=$FIRMWARE_VERSION" >> "$GITHUB_ENV" echo "RELEASE_VERSION=$RELEASE_VERSION" >> "$GITHUB_ENV" diff --git a/.github/workflows/eastmesh-build-companion-wifi-firmwares.yml b/.github/workflows/eastmesh-build-companion-wifi-firmwares.yml deleted file mode 100644 index 4f2cf517..00000000 --- a/.github/workflows/eastmesh-build-companion-wifi-firmwares.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: EastMesh Build Companion WiFi Firmwares - -permissions: - contents: write - -on: - workflow_dispatch: - push: - tags: - - "companion-wifi-v*" - -jobs: - - build: - runs-on: ubuntu-latest - env: - OFFICIAL_MESHCORE_VERSION: ${{ vars.OFFICIAL_MESHCORE_VERSION }} - steps: - - - name: Clone Repo - uses: actions/checkout@v6 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-environment - - - name: Build Firmwares - run: /usr/bin/env bash eastmesh-build.sh build-companion-wifi-firmwares - - - name: Upload Workflow Artifacts - uses: actions/upload-artifact@v7 - with: - name: companion-wifi-firmwares - path: out - - - name: Create Release - uses: softprops/action-gh-release@v3 - if: startsWith(github.ref, 'refs/tags/') - with: - name: Companion WiFi Firmware ${{ env.RELEASE_VERSION }} - body: "" - draft: true - files: out/* diff --git a/.github/workflows/eastmesh-build-observer-espnow-firmwares.yml b/.github/workflows/eastmesh-build-observer-espnow-firmwares.yml deleted file mode 100644 index 036b6ccd..00000000 --- a/.github/workflows/eastmesh-build-observer-espnow-firmwares.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: EastMesh Build Observer Bridge ESP-NOW Firmwares - -permissions: - contents: write - -on: - workflow_dispatch: - push: - tags: - - "observer-eastmesh-bridge-espnow-v*" - -jobs: - build: - runs-on: ubuntu-latest - env: - OFFICIAL_MESHCORE_VERSION: ${{ vars.OFFICIAL_MESHCORE_VERSION }} - PLATFORMIO_BUILD_FLAGS: -UMQTT_DEBUG - steps: - - name: Clone Repo - uses: actions/checkout@v6 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-environment - - - name: Build Firmwares - run: /usr/bin/env bash eastmesh-build.sh build-observer-espnow-firmwares - - - name: Upload Workflow Artifacts - uses: actions/upload-artifact@v7 - with: - name: observer-bridge-espnow-firmwares - path: out - - - name: Create Release - uses: softprops/action-gh-release@v3 - if: startsWith(github.ref, 'refs/tags/') - with: - name: Observer EastMesh Firmware with ESP-NOW Bridge ${{ env.RELEASE_VERSION }} - body: "" - draft: true - files: out/* diff --git a/.github/workflows/eastmesh-build-observer-firmwares.yml b/.github/workflows/eastmesh-build-observer-firmwares.yml deleted file mode 100644 index ec613229..00000000 --- a/.github/workflows/eastmesh-build-observer-firmwares.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: EastMesh Build Observer Firmwares - -permissions: - contents: write - -on: - workflow_dispatch: - push: - tags: - - "observer-eastmesh-v*" - -jobs: - build: - runs-on: ubuntu-latest - env: - OFFICIAL_MESHCORE_VERSION: ${{ vars.OFFICIAL_MESHCORE_VERSION }} - PLATFORMIO_BUILD_FLAGS: -UMQTT_DEBUG - steps: - - name: Clone Repo - uses: actions/checkout@v6 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-environment - - - name: Build Firmwares - run: /usr/bin/env bash eastmesh-build.sh build-observer-firmwares - - - name: Upload Workflow Artifacts - uses: actions/upload-artifact@v7 - with: - name: observer-firmwares - path: out - - - name: Create Release - uses: softprops/action-gh-release@v3 - if: startsWith(github.ref, 'refs/tags/') - with: - name: Observer EastMesh Firmware ${{ env.RELEASE_VERSION }} - body: "" - draft: true - files: out/* diff --git a/.github/workflows/eastmesh-build-observer-mqtt-bridge-firmwares.yml b/.github/workflows/eastmesh-build-observer-mqtt-bridge-firmwares.yml deleted file mode 100644 index 3b47c631..00000000 --- a/.github/workflows/eastmesh-build-observer-mqtt-bridge-firmwares.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: EastMesh Build Observer MQTT Bridge Firmwares - -permissions: - contents: write - -on: - workflow_dispatch: - push: - tags: - - "observer-eastmesh-bridge-mqtt-v*" - -jobs: - build: - runs-on: ubuntu-latest - env: - OFFICIAL_MESHCORE_VERSION: ${{ vars.OFFICIAL_MESHCORE_VERSION }} - PLATFORMIO_BUILD_FLAGS: -UMQTT_DEBUG - steps: - - name: Clone Repo - uses: actions/checkout@v6 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-environment - - - name: Build Firmwares - run: /usr/bin/env bash eastmesh-build.sh build-observer-mqtt-bridge-firmwares - - - name: Upload Workflow Artifacts - uses: actions/upload-artifact@v7 - with: - name: observer-bridge-mqtt-firmwares - path: out - - - name: Create Release - uses: softprops/action-gh-release@v3 - if: startsWith(github.ref, 'refs/tags/') - with: - name: Observer EastMesh Firmware with MQTT Bridge ${{ env.RELEASE_VERSION }} - body: "" - draft: true - files: out/* diff --git a/.github/workflows/eastmesh-build-repeater-bridge-espnow-firmwares.yml b/.github/workflows/eastmesh-build-repeater-bridge-espnow-firmwares.yml deleted file mode 100644 index 716b512c..00000000 --- a/.github/workflows/eastmesh-build-repeater-bridge-espnow-firmwares.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: EastMesh Build Repeater ESP-NOW Bridge Firmwares - -permissions: - contents: write - -on: - workflow_dispatch: - push: - tags: - - "repeater-bridge-espnow-v*" - -jobs: - - build: - runs-on: ubuntu-latest - env: - OFFICIAL_MESHCORE_VERSION: ${{ vars.OFFICIAL_MESHCORE_VERSION }} - steps: - - - name: Clone Repo - uses: actions/checkout@v6 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-environment - - - name: Build Firmwares - run: /usr/bin/env bash eastmesh-build.sh build-repeater-bridge-espnow-firmwares - - - name: Upload Workflow Artifacts - uses: actions/upload-artifact@v7 - with: - name: repeater-bridge-espnow-firmwares - path: out - - - name: Create Release - uses: softprops/action-gh-release@v3 - if: startsWith(github.ref, 'refs/tags/') - with: - name: Repeater ESP-NOW Bridge Firmware ${{ env.RELEASE_VERSION }} - body: "" - draft: true - files: out/* diff --git a/.github/workflows/eastmesh-github-pages.yml b/.github/workflows/eastmesh-github-pages.yml deleted file mode 100644 index be77644d..00000000 --- a/.github/workflows/eastmesh-github-pages.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: EastMesh Build and deploy Docs site to GitHub Pages - -on: - workflow_dispatch: - push: - branches: - - main - -permissions: - contents: write - -jobs: - github-pages: - runs-on: ubuntu-latest - steps: - - name: Checkout Repo - uses: actions/checkout@v6.0.2 - - - name: Setup Python - uses: actions/setup-python@v6.2.0 - with: - python-version: "3.13" - - - name: Install uv - uses: astral-sh/setup-uv@v8.0.0 - - - name: Sync Docs Tooling - run: uv sync --locked --group docs - - - name: Build - run: uv run --group docs zensical build - - - name: Write CNAME - if: ${{ vars.PAGES_CNAME != '' }} - run: printf '%s\n' "${{ vars.PAGES_CNAME }}" > site/CNAME - - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v4.0.0 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./site - publish_branch: "gh-pages" diff --git a/.github/workflows/eastmesh-pr-build-check.yml b/.github/workflows/eastmesh-pr-build-check.yml deleted file mode 100644 index cdc6d5eb..00000000 --- a/.github/workflows/eastmesh-pr-build-check.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: EastMesh PR Build Check - -on: - pull_request: - branches: [ main ] - paths: - - "src/**" - - "examples/**" - - "variants/**" - - "platformio.ini" - - ".github/workflows/eastmesh-pr-build-check.yml" - -jobs: - build: - runs-on: ubuntu-latest - env: - OFFICIAL_MESHCORE_VERSION: ${{ vars.OFFICIAL_MESHCORE_VERSION }} - PLATFORMIO_BUILD_FLAGS: -UMQTT_DEBUG - strategy: - fail-fast: false - matrix: - environment: - # ESP32-S3 / core EastMesh targets - - heltec_v4_companion_radio_wifi - - heltec_v4_repeater_observer - - LilyGo_TBeam_1W_companion_radio_wifi - - LilyGo_TBeam_1W_repeater_observer - - T_Beam_S3_Supreme_SX1262_companion_radio_wifi - - T_Beam_S3_Supreme_SX1262_repeater_observer - - steps: - - name: Clone Repo - uses: actions/checkout@v6 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-environment - - - name: Build ${{ matrix.environment }} - run: pio run -e ${{ matrix.environment }} diff --git a/.github/workflows/eastmesh-push-build-check.yml b/.github/workflows/eastmesh-push-build-check.yml deleted file mode 100644 index fc757fc6..00000000 --- a/.github/workflows/eastmesh-push-build-check.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: EastMesh PUSH Build Check - -on: - push: - branches: [ develop ] - paths: - - "src/**" - - "examples/**" - - "variants/**" - - "platformio.ini" - - ".github/workflows/eastmesh-push-build-check.yml" - -jobs: - build: - runs-on: ubuntu-latest - env: - OFFICIAL_MESHCORE_VERSION: ${{ vars.OFFICIAL_MESHCORE_VERSION }} - PLATFORMIO_BUILD_FLAGS: -UMQTT_DEBUG - strategy: - fail-fast: false - matrix: - environment: - # ESP32-S3 / core EastMesh targets - - heltec_v4_repeater_observer - - LilyGo_TBeam_1W_repeater_observer - - T_Beam_S3_Supreme_SX1262_repeater_observer - # ESP32-C6 - - LilyGo_Tlora_C6_repeater_observer - - Xiao_C6_repeater_observer - - steps: - - name: Clone Repo - uses: actions/checkout@v6 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-environment - - - name: Build ${{ matrix.environment }} - run: pio run -e ${{ matrix.environment }} diff --git a/AGENTS.md b/AGENTS.md index c902f7d1..9321d676 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,70 +1,70 @@ # AGENTS.md -## Purpose +## Назначение -EastMesh layer on top of upstream MeshCore. +Слой PosadMesh поверх upstream MeshCore. -**Default:** preserve upstream behavior. -Only modify code for clearly scoped EastMesh features: +**По умолчанию:** сохраняем поведение upstream. +Изменяем код только для чётко ограниченных возможностей PosadMesh: - `*_repeater_observer` - `*_companion_radio_wifi` -- MQTT uplink/broker -- repeater web panel -- docs, releases, automation +- MQTT-аплинк/брокер +- веб-панель репитера +- документация, релизы, автоматизация -## Principles +## Принципы -- Minimal, targeted changes -- Prefer additive over modifying upstream code -- Avoid unrelated refactors -- Maintain parity with upstream behavior +- Минимальные точечные изменения +- Предпочитаем дополнять, а не менять код upstream +- Избегаем посторонних рефакторингов +- Сохраняем паритет с поведением upstream -## Guardrails +## Ограничения ### Upstream -- Do not modify unrelated MeshCore logic -- Do not change CLI semantics unless explicitly required -- Do not introduce breaking changes to existing targets +- Не менять постороннюю логику MeshCore +- Не менять семантику CLI без явной необходимости +- Не вносить ломающие изменения в существующие таргеты -### Docs (update in same PR when practical) +### Документация (обновлять в том же PR, когда это возможно) -| Change | File | -| --------------------- | -------------------- | -| CLI | `eastmesh-docs/custom-cli.md` | -| Web panel UI/behavior | `eastmesh-docs/web-panel.md` | -| Releases | `release-notes.yml` | -| Flashing guidance | `eastmesh-docs/releases.md` | +| Изменение | Файл | +| --------------------- | ----------------------------- | +| CLI | `posadmesh-docs/custom-cli.md` | +| UI/поведение веб-панели | `posadmesh-docs/web-panel.md` | +| Релизы | `release-notes.yml` | +| Руководство по прошивке | `posadmesh-docs/releases.md` | -### Web Panel Gate +### Контроль веб-панели -If editing `examples/simple_repeater/MyMesh.cpp`, also update `eastmesh-docs/custom-cli.md`. +Если правите `examples/simple_repeater/MyMesh.cpp`, обновите также `posadmesh-docs/custom-cli.md`. -## Tooling +## Инструменты -- Use `uv` + PlatformIO via `uv run` -- Do not assume global `pio` +- Использовать `uv` + PlatformIO через `uv run` +- Не рассчитывать на глобально установленный `pio` -### Build Policy +### Политика сборок -Builds are expensive. Avoid unless necessary. +Сборки дорогие. Избегать без необходимости. -Do NOT build for: +НЕ собирать для: -- docs / HTML / CSS only +- только документация / HTML / CSS -Prefer: +Предпочитать: -- user-run local builds -- reasoning over execution +- локальные сборки, запускаемые пользователем +- рассуждения вместо выполнения -Build only if: +Собирать только если: -- high-risk change -- firmware behavior must be verified +- изменение высокого риска +- нужно проверить поведение прошивки -### Commands +### Команды ```bash uv sync @@ -74,17 +74,17 @@ uv run --group docs zensical serve uv run --group docs zensical build ``` -Do not assume `pio` is installed globally. +Не рассчитывайте, что `pio` установлен глобально. -## Common Commands +## Частые команды -List build targets: +Показать список таргетов сборки: ```bash -bash eastmesh-build.sh list +bash posadmesh-build.sh list ``` -Build a single target: +Собрать один таргет: ```bash uv run pio run -e heltec_v4_repeater_observer @@ -93,143 +93,144 @@ uv run pio run -e heltec_v4_companion_radio_wifi uv run pio run -e T_Beam_S3_Supreme_SX1262_companion_radio_wifi ``` -Build with release-style metadata: +Сборка с релизными метаданными: ```bash export FIRMWARE_VERSION=v1.15.0 -export EASTMESH_VERSION=v2026.5.1 -bash eastmesh-build.sh build-firmware heltec_v4_repeater_observer -bash eastmesh-build.sh build-firmware T_Beam_S3_Supreme_SX1262_repeater_observer +export POSADMESH_VERSION=v2026.5.1 +bash posadmesh-build.sh build-firmware heltec_v4_repeater_observer +bash posadmesh-build.sh build-firmware T_Beam_S3_Supreme_SX1262_repeater_observer ``` -Flash a target: +Прошить таргет: ```bash uv run pio run -e heltec_v4_repeater_observer -t upload --upload-port /dev/tty.usbmodemXXXX uv run pio run -e T_Beam_S3_Supreme_SX1262_repeater_observer -t upload --upload-port /dev/tty.usbmodemXXXX ``` -## Key Files +## Ключевые файлы -- `eastmesh-build.sh` — EastMesh build wrapper -- `build.sh` — upstream MeshCore build wrapper retained for merge hygiene +- `posadmesh-build.sh` — обёртка сборки PosadMesh +- `build.sh` — обёртка сборки upstream MeshCore, сохранена ради чистоты слияний - `platformio.ini` -- `variants/eastmesh/platformio.ini` +- `variants/posadmesh_mqtt/platformio.ini` - `examples/simple_repeater/MyMesh.cpp` - `src/helpers/mqtt/MQTTUplink.cpp` -- `eastmesh-docs/*.md` +- `posadmesh-docs/*.md` - `RELEASE.md` - `release-notes.yml` -## Workflow Ownership +## Принадлежность workflow -EastMesh workflows use the `eastmesh-*.yml` prefix in `.github/workflows/`. +Репозиторий живёт на . Собственные workflow GitHub Actions (`posadmesh-*.yml`) удалены при переезде: релизы собираются локально через `posadmesh-build.sh`, теги и релизы создаются вручную в веб-интерфейсе git.meshinfo.ru. -Upstream MeshCore workflows may remain under their original filenames for merge hygiene. Do not adapt them for EastMesh behavior; keep them close to upstream and disable them in GitHub Actions for this repository. +Workflow upstream MeshCore остаются под исходными именами файлов ради чистоты слияний. Не адаптируйте их под поведение PosadMesh; держите их близко к upstream. Они не используются для релизов PosadMesh. -## Docs Sync Requirements +## Требования к синхронизации документации -If you change any of the following, update docs in the same PR when practical: +Если вы меняете что-либо из перечисленного, обновляйте документацию в том же PR, когда это возможно: -- Web-panel commands: - - update `eastmesh-docs/custom-cli.md` -- Web-panel user-facing behavior, sections, controls, or troubleshooting: - - update `eastmesh-docs/web-panel.md` -- EastMesh CLI additions or changed semantics: - - update `eastmesh-docs/custom-cli.md` -- Release/tag preparation: - - update `release-notes.yml` -- Flashing/release asset guidance: - - update `eastmesh-docs/releases.md` +- Команды веб-панели: + - обновить `posadmesh-docs/custom-cli.md` +- Пользовательское поведение, разделы, элементы управления или диагностика веб-панели: + - обновить `posadmesh-docs/web-panel.md` +- Добавления в CLI PosadMesh или изменения семантики: + - обновить `posadmesh-docs/custom-cli.md` +- Подготовка релиза/тега: + - обновить `release-notes.yml` +- Руководство по прошивке и файлам релиза: + - обновить `posadmesh-docs/releases.md` -## Observer Notes +## Замечания по обсерверам -`*_repeater_observer` builds may include the local HTTPS web panel on supported ESP32 targets. +Сборки `*_repeater_observer` могут включать локальную HTTPS-веб-панель на поддерживаемых таргетах ESP32. -Operational guidance already reflected in docs: +Эксплуатационные рекомендации уже отражены в документации: -- use for initial setup and troubleshooting -- prefer `set web off` afterward for maximum heap headroom +- использовать для первичной настройки и диагностики +- после этого предпочитать `set web off` для максимального запаса heap -## Companion WiFi Notes +## Замечания по companion WiFi -`*_companion_radio_wifi` targets support persisted Wi-Fi rescue commands via serial `CLI Rescue`. +Таргеты `*_companion_radio_wifi` поддерживают сохраняемые команды восстановления Wi-Fi через последовательный `CLI Rescue`. -Do not document companion rescue commands in repeater docs. Do not assume web-panel behavior applies. +Не документируйте команды восстановления companion в документации репитера. Не предполагайте, что поведение веб-панели применимо. -Companion release/version rule: +Правило версий companion: -- companion tags use the official upstream MeshCore release version only -- the current official MeshCore version is `v1.15.0` -- companion releases are only cut when `meshcore-dev/MeshCore` has made an official release -- do not invent separate EastMesh companion version numbers +- теги companion используют только официальную версию релиза upstream MeshCore +- текущая официальная версия MeshCore — `v1.15.0` +- companion-релизы выпускаются только когда `meshcore-dev/MeshCore` сделал официальный релиз +- не придумывайте отдельные номера версий PosadMesh для companion -## Release Workflow +## Процесс релиза -Current tag formats: +Текущие форматы тегов: ```bash git tag companion-wifi-v1.15.0 git tag repeater-bridge-espnow-v1.15.0 -git tag observer-eastmesh-bridge-espnow-v2026.5.1 -git tag observer-eastmesh-bridge-mqtt-v2026.7.0 -git tag observer-eastmesh-v2026.5.1 +git tag observer-posadmesh-bridge-espnow-v2026.5.1 +git tag observer-posadmesh-bridge-mqtt-v2026.7.0 +git tag observer-posadmesh-v2026.5.1 ``` -Rules: +Правила: -- `companion-wifi` tags use the upstream MeshCore version directly -- `repeater-bridge-espnow` tags use the upstream MeshCore version directly -- `observer-eastmesh-bridge-espnow` tags use the EastMesh release version in the tag -- `observer-eastmesh-bridge-mqtt` tags use the EastMesh release version in the tag -- `observer-eastmesh` tags use the EastMesh release version in the tag -- GitHub Actions variable `OFFICIAL_MESHCORE_VERSION` supplies the upstream base version for Observer EastMesh, Observer ESP-NOW EastMesh, and Observer MQTT Bridge EastMesh release builds -- if the upstream MeshCore release version changes, update `OFFICIAL_MESHCORE_VERSION` in GitHub before cutting release tags +- теги `companion-wifi` используют версию upstream MeshCore напрямую +- теги `repeater-bridge-espnow` используют версию upstream MeshCore напрямую +- теги `observer-posadmesh-bridge-espnow` используют версию релиза PosadMesh в теге +- теги `observer-posadmesh-bridge-mqtt` используют версию релиза PosadMesh в теге +- теги `observer-posadmesh` используют версию релиза PosadMesh в теге +- переменная `FIRMWARE_VERSION` при локальной сборке задаёт базовую версию upstream для релизных сборок треков `observer-posadmesh`, `observer-posadmesh-bridge-espnow` и `observer-posadmesh-bridge-mqtt` +- переменная `POSADMESH_VERSION` при локальной сборке задаёт версию релиза PosadMesh +- если версия релиза upstream MeshCore изменилась, обновите `FIRMWARE_VERSION` при сборке и в документации -Typical release flow: +Типичный порядок выпуска: -1. Update `OFFICIAL_MESHCORE_VERSION` if upstream changed. -2. Update `release-notes.yml` on `develop`. -3. Merge the release PR from `develop` to `main`. -4. Create the desired release tag or tags on the target commit on `main`. -5. Push the tags. +1. Обновить `release-notes.yml` в ветке `develop`. +2. Смерджить релизный PR из `develop` в `main`. +3. Собрать нужные прошивки локально с подходящими `FIRMWARE_VERSION` и `POSADMESH_VERSION`. +4. Создать нужный тег или теги релиза на целевом коммите в `main`. +5. Запушить теги в git.meshinfo.ru и создать релиз в его веб-интерфейсе. -## Upstream Sync Workflow +## Синхронизация с upstream -When asked to pull from upstream MeshCore: +Когда просят подтянуть изменения из upstream MeshCore: -- pull from `meshcore-dev/MeshCore:dev` -- start from local `develop` -- create a temporary integration branch off `develop` -- merge upstream `dev` into that temporary integration branch -- resolve conflicts in a way that preserves EastMesh-specific changes -- merge the finished integration branch back into `develop` +- тянуть из `meshcore-dev/MeshCore:dev` +- начинать с локальной ветки `develop` +- создать временную интеграционную ветку от `develop` +- влить upstream `dev` в эту временную интеграционную ветку +- разрешать конфликты так, чтобы сохранить изменения, специфичные для PosadMesh +- влить готовую интеграционную ветку обратно в `develop` -Do not merge upstream directly into `main`. +Не вливайте upstream напрямую в `main`. -## Scope Boundaries +## Границы области работ -Do NOT (unless asked): +НЕ делать (без явной просьбы): -- rename tracks -- change tag formats -- expand cli without updating docs -- change upstream CLI semantics -- introduce new versioning schemes +- переименовывать треки +- менять форматы тегов +- расширять CLI без обновления документации +- менять семантику CLI upstream +- вводить новые схемы версионирования -## Commit Messages +## Сообщения коммитов -Use concise, conventional prefixes: +Используйте краткие общепринятые префиксы: -- `feat:` new functionality -- `fix:` bug fixes -- `docs:` documentation changes -- `chore:` maintenance, tooling, non-functional -- `refactor:` code changes without behavior change +- `feat:` новая функциональность +- `fix:` исправления ошибок +- `docs:` изменения документации +- `chore:` обслуживание, инструменты, нефункциональное +- `refactor:` изменения кода без изменения поведения -Keep messages short and scoped. +Держите сообщения короткими и по делу. -## Decision Rule +## Правило принятия решений -If a change is not clearly EastMesh-specific, do not modify the code. -When uncertain, prefer no change or request clarification. +Если изменение не является явно специфичным для PosadMesh, не меняйте код. +При сомнениях предпочитайте не менять ничего или запросить уточнение. diff --git a/README.md b/README.md index fbacd048..cbd2f7ad 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,54 @@ -# MeshCore-EastMesh +# MeshCore-Posadmesh -MeshCore-EastMesh originates from the upstream MeshCore project: +MeshCore-Posadmesh основан на upstream-проекте MeshCore: - -Credits to Scott Powell / Ripple Radios and the MeshCore contributors for the original firmware and project foundation. +Благодарность Scott Powell / Ripple Radios и участникам MeshCore за оригинальную прошивку и основу проекта. -EastMesh is a community-driven mesh network across Eastern Australia. This repository provides tailored builds of MeshCore, including WiFi companion firmware with additional CLI tools and Observer firmware that integrates with our CoreScope telemetry platform. +PosadMesh — это mesh-сеть, развиваемая сообществом на востоке Австралии. Этот репозиторий предоставляет адаптированные сборки MeshCore, включая прошивку WiFi companion с дополнительными инструментами CLI и прошивку обсервера, которая интегрируется с нашей платформой телеметрии CoreScope. -CoreScope () offers visibility into the network, including repeater status, observers, and mapping data. +CoreScope () обеспечивает видимость сети, включая состояние репитеров, обсерверов и данные картографирования. -**Observer builds feed EastMesh Core telemetry and are intended for Eastern Australia repeaters only**; running them elsewhere can skew shared network data. +**Сборки обсервера передают телеметрию в PosadMesh Core и предназначены только для репитеров Восточной Австралии**; запуск их в других местах может исказить общие данные сети. -## What This Repo Adds +## Что добавляет этот репозиторий -- `*_repeater_observer` firmware targets with: - - native WiFi - - MQTT over WSS with JWT auth - - optional local HTTPS config panel on supported ESP32 targets -- `*_repeater_observer_espnow` firmware targets that add a local ESP-NOW mesh bridge -- `*_repeater_bridge_espnow` firmware targets for a plain upstream-style repeater ESP-NOW bridge, without MQTT uplink or the EastMesh web panel -- `*_companion_radio_wifi` firmware targets for WiFi-connected companion devices -- EastMesh-specific release workflows and versioning on top of upstream MeshCore releases -- docs and release guidance for EastMesh users instead of the full upstream MeshCore docs set +- таргеты прошивки `*_repeater_observer` с: + - встроенным WiFi + - MQTT-аплинком к курируемому брокеру MeshCoreTel и дополнительно опциональным собственным брокером (TCP или WSS) + - опциональной локальной панелью конфигурации HTTPS на поддерживаемых таргетах ESP32 +- таргеты прошивки `*_repeater_observer_espnow`, добавляющие локальный мост ESP-NOW mesh +- таргеты прошивки `*_repeater_bridge_espnow` для обычного ESP-NOW-моста репитера в стиле upstream, без MQTT-аплинка и веб-панели PosadMesh +- таргеты прошивки `*_companion_radio_wifi` для companion-устройств с подключением по WiFi +- специфичные для PosadMesh процесс релизов и версионирование поверх релизов upstream MeshCore +- документация и руководство по релизам для пользователей PosadMesh вместо полного набора документации upstream MeshCore -## Releases +## Релизы -Prebuilt firmware is published on GitHub Releases: +Готовые прошивки публикуются в разделе Releases на git.meshinfo.ru: -- +- -For flashing guidance, including when to use `.bin` vs `-merged.bin`, see: +Руководство по прошивке, в том числе когда использовать `.bin`, а когда `-merged.bin`, см. в: -- [eastmesh-docs/releases.md](./eastmesh-docs/releases.md) +- [posadmesh-docs/releases.md](./posadmesh-docs/releases.md) -The custom firmware flasher site is: +Сайт прошивальщика кастомной прошивки: - -## Install uv +## Установка uv -This repo uses `uv` for Python tooling and runs PlatformIO through `uv run`. +Этот репозиторий использует `uv` для Python-инструментов и запускает PlatformIO через `uv run`. -Official install docs: +Официальная документация по установке: - -Common install methods: +Распространённые способы установки: -macOS and Linux: +macOS и Linux: ```bash curl -LsSf https://astral.sh/uv/install.sh | sh @@ -66,143 +66,138 @@ Windows PowerShell: powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` -## Common Repo Usage +## Типовое использование репозитория -Install tooling: +Установка инструментов: ```bash uv sync ``` -List available build targets: +Список доступных таргетов сборки: ```bash -bash eastmesh-build.sh list +bash posadmesh-build.sh list ``` -Build a single target: +Сборка одного таргета: ```bash uv run pio run -e heltec_v4_repeater_observer uv run pio run -e heltec_v4_companion_radio_wifi ``` -Build with release-style version metadata: +Сборка с релизными метаданными версии: ```bash export FIRMWARE_VERSION=v1.15.0 -export EASTMESH_VERSION=v2026.5.1 -bash eastmesh-build.sh build-firmware heltec_v4_repeater_observer +export POSADMESH_VERSION=v2026.5.1 +bash posadmesh-build.sh build-firmware heltec_v4_repeater_observer ``` -Flash a target: +Прошивка таргета: ```bash uv run pio run -e heltec_v4_repeater_observer -t upload --upload-port /dev/tty.usbmodemXXXX ``` -Open a serial monitor: +Открыть последовательный монитор: ```bash uv run pio device monitor --port /dev/tty.usbmodemXXXX --baud 115200 ``` -Build docs locally: +Локальная сборка документации: ```bash uv run --group docs zensical serve uv run --group docs zensical build ``` -## Key Files +## Ключевые файлы -- [`eastmesh-build.sh`](./eastmesh-build.sh) - - EastMesh local/release build wrapper - - injects `FIRMWARE_VERSION`, `CLIENT_VERSION`, and EastMesh release metadata +- [`posadmesh-build.sh`](./posadmesh-build.sh) + - обёртка локальной/релизной сборки PosadMesh + - внедряет `FIRMWARE_VERSION`, `CLIENT_VERSION` и релизные метаданные PosadMesh - [`build.sh`](./build.sh) - - upstream MeshCore build wrapper retained to reduce future merge conflicts + - обёртка сборки upstream MeshCore, сохранённая для уменьшения будущих конфликтов слияния - [`pyproject.toml`](./pyproject.toml) - - Python tooling and docs dependencies + - Python-инструменты и зависимости для документации - [`platformio.ini`](./platformio.ini) - - root PlatformIO config and ESP32 helper scripts -- [`variants/eastmesh/platformio.ini`](./variants/eastmesh/platformio.ini) - - shared EastMesh observer env definitions + - корневой конфиг PlatformIO и вспомогательные скрипты ESP32 +- [`variants/posadmesh_mqtt/platformio.ini`](./variants/posadmesh_mqtt/platformio.ini) + - общие определения окружений обсервера PosadMesh - [`examples/simple_repeater/MyMesh.cpp`](./examples/simple_repeater/MyMesh.cpp) - - repeater CLI wiring, MQTT command surface, and web allowlist integration + - подключение CLI репитера, набор MQTT-команд и интеграция с белым списком веб-панели - [`src/helpers/bridges/MQTTBridge.cpp`](./src/helpers/bridges/MQTTBridge.cpp) - - bidirectional MQTT mesh bridge (peer broker TCP, topic `meshcore/bridge/packets`) + - двунаправленный MQTT-мост mesh (peer-брокер TCP, топик `meshcore/bridge/packets`) - [`src/helpers/mqtt/MQTTUplink.cpp`](./src/helpers/mqtt/MQTTUplink.cpp) - - MQTT uplink implementation, HTTPS web panel, WSS/JWT handling, and repeater WiFi control + - реализация MQTT-аплинка, веб-панель HTTPS, поддержка собственного брокера (TCP/WSS) и управление WiFi репитера - [`examples/companion_radio`](./examples/companion_radio) - - companion firmware implementation + - реализация прошивки companion - [`RELEASE.md`](./RELEASE.md) - - tag formats and release workflow behavior -- [`eastmesh-docs/`](./eastmesh-docs) - - EastMesh-focused docs published to GitHub Pages + - форматы тегов и порядок выпуска релизов +- [`posadmesh-docs/`](./posadmesh-docs) + - документация, ориентированная на PosadMesh, для чтения в веб-интерфейсе git.meshinfo.ru - [`docs/`](./docs) - - upstream MeshCore docs retained to reduce future merge conflicts + - документация upstream MeshCore, сохранённая для уменьшения будущих конфликтов слияния -## Key EastMesh Features +## Ключевые возможности PosadMesh -### MQTT Repeater Additions +### Дополнения MQTT для репитера -- curated broker support for: - - `eastmesh-au` - - `meshmapper` - - `waev` - - `letsmesh-eu` (retired) - - `letsmesh-us` (retired) -- WSS transport at `/mqtt` -- JWT auth using the device identity -- CLI controls for: - - WiFi credentials - - WiFi powersaving - - board battery reporting on supported targets - - MQTT endpoint enablement - - MQTT packet and raw publishing - - owner public key and email - - local web panel enablement +- поддержка курируемого брокера: + - `meshcoretel` +- опциональный собственный брокер (TCP или MQTT поверх WSS по пути `/mqtt`) с именем пользователя и паролем +- элементы управления в CLI для: + - учётных данных WiFi + - энергосбережения WiFi + - отчётов о заряде батареи платы на поддерживаемых таргетах + - включения MQTT-эндпоинта + - публикации MQTT-пакетов и raw-данных + - публичного ключа и email владельца + - включения локальной веб-панели -### MQTT Mesh Bridge (Observer) +### MQTT-мост mesh (обсервер) -The `observer-eastmesh-bridge-mqtt` release track (`*_repeater_observer_mqtt_bridge`) bridges **raw mesh packets** between repeaters through a **peer MQTT broker you run** (for example Mosquitto on a LAN PC). This is **not** the same as MQTT uplink to EastMesh/MeshMapper: +Трек релиза `observer-posadmesh-bridge-mqtt` (`*_repeater_observer_mqtt_bridge`) выполняет мостовую передачу **raw-пакетов mesh** между репитерами через **peer-брокер MQTT, который запускаете вы сами** (например, Mosquitto на ПК в локальной сети). Это **не** то же самое, что MQTT-аплинк к курируемому брокеру MeshCoreTel: -| | MQTT uplink (observer) | MQTT mesh bridge | +| | MQTT-аплинк (обсервер) | MQTT-мост mesh | | --- | --- | --- | -| Purpose | Publish JSON telemetry to curated brokers | Forward encrypted mesh packets between radios | -| Brokers | `eastmesh-au`, `meshmapper`, custom WSS | Your peer broker (`host:port`, TCP) | -| Topic | `meshcore//...` (per IATA) | `meshcore/bridge/packets` (fixed) | -| Default on bridge builds | Off by default (enable in web panel / CLI) | On when `bridge.enabled` is on | +| Назначение | Публикация JSON-телеметрии в курируемые брокеры | Передача зашифрованных пакетов mesh между радиоустройствами | +| Брокеры | `meshcoretel`, собственный брокер | Ваш peer-брокер (`host:port`, TCP) | +| Топик | `meshcore//...` (по IATA) | `meshcore/bridge/packets` (фиксированный) | +| По умолчанию в bridge-сборках | Выключено по умолчанию (включается в веб-панели / CLI) | Включено, когда включён `bridge.enabled` | -**First supported target:** `Xiao_S3_WIO_repeater_observer_mqtt_bridge` +**Первый поддерживаемый таргет:** `Xiao_S3_WIO_repeater_observer_mqtt_bridge` -**Related track:** `observer-eastmesh-bridge-espnow` (`*_repeater_observer_espnow`) uses ESP-NOW instead of MQTT for local bridging. +**Связанный трек:** `observer-posadmesh-bridge-espnow` (`*_repeater_observer_espnow`) использует ESP-NOW вместо MQTT для локальной мостовой передачи. -Build example: +Пример сборки: ```bash uv run pio run -e Xiao_S3_WIO_repeater_observer_mqtt_bridge ``` -Flash (update): +Прошивка (обновление): ```bash uv run pio run -e Xiao_S3_WIO_repeater_observer_mqtt_bridge -t upload --upload-port COM10 ``` -After a **full flash erase**, flash the merged image at `0x0` instead of only `firmware.bin`: +После **полного стирания флеш-памяти** прошивайте объединённый образ по адресу `0x0`, а не только `firmware.bin`: ```bash uv run pio run -e Xiao_S3_WIO_repeater_observer_mqtt_bridge -t mergebin uv run pio pkg exec -p tool-esptoolpy -- esptool.py --chip esp32s3 --port COM10 write_flash 0x0 .pio/build/Xiao_S3_WIO_repeater_observer_mqtt_bridge/firmware-merged.bin ``` -#### Peer Mosquitto broker (LAN) +#### Peer-брокер Mosquitto (локальная сеть) -Each bridge node connects to the **same** peer broker with the **same** credentials and `bridge.secret`. +Каждый узел моста подключается к **одному и тому же** peer-брокеру с **одними и теми же** учётными данными и `bridge.secret`. -1. Install [Mosquitto](https://mosquitto.org/download/) on a machine reachable from both repeaters (for example `192.168.1.145`). -2. Edit `mosquitto.conf` (Windows service install: `C:\Program Files\Mosquitto\mosquitto.conf`) and add: +1. Установите [Mosquitto](https://mosquitto.org/download/) на машине, доступной с обоих репитеров (например, `192.168.1.145`). +2. Отредактируйте `mosquitto.conf` (при установке службой Windows: `C:\Program Files\Mosquitto\mosquitto.conf`) и добавьте: ```conf listener 1883 0.0.0.0 @@ -210,43 +205,43 @@ allow_anonymous false password_file C:\Program Files\Mosquitto\passwd ``` -For lab/testing only, `allow_anonymous true` works without a password file. +Только для лабораторных/тестовых целей `allow_anonymous true` работает и без файла паролей. -3. Create a user (admin CMD): +3. Создайте пользователя (CMD от имени администратора): ```cmd cd "C:\Program Files\Mosquitto" mosquitto_passwd -c passwd bridgeuser ``` -4. Restart the **service** (do not run a second `mosquitto -v` while the service owns port 1883): +4. Перезапустите **службу** (не запускайте второй `mosquitto -v`, пока служба занимает порт 1883): ```powershell Restart-Service mosquitto ``` -5. Confirm LAN listen and open the firewall: +5. Убедитесь, что прослушивание идёт в локальной сети, и откройте брандмауэр: ```cmd netstat -ano | findstr ":1883" ``` -Expect `0.0.0.0:1883`, not only `127.0.0.1:1883`. +Ожидается `0.0.0.0:1883`, а не только `127.0.0.1:1883`. -6. Test from another machine: +6. Проверьте с другой машины: ```cmd mosquitto_pub -h 192.168.1.145 -p 1883 -u bridgeuser -P your-password -t test -m hello mosquitto_sub -h 192.168.1.145 -p 1883 -u bridgeuser -P your-password -t meshcore/bridge/packets -v ``` -Binary garbage on `meshcore/bridge/packets` is normal — payloads are XOR-encrypted mesh frames, not text. +Двоичный мусор в `meshcore/bridge/packets` — это нормально: payload — это XOR-зашифрованные кадры mesh, а не текст. -#### Repeater configuration +#### Настройка репитера -Configure **both** bridge nodes identically for broker access; use the same `bridge.secret` on every node in the bridge group. +Настройте **оба** узла моста одинаково для доступа к брокеру; используйте один и тот же `bridge.secret` на каждом узле группы моста. -Serial or web panel CLI: +CLI через последовательный порт или веб-панель: ```text set wifi.ssid YourNetwork @@ -258,9 +253,9 @@ set bridge.peer.password your-password set bridge.secret your-shared-bridge-secret ``` -Web panel: **MQTT Settings** → **Mesh bridge peer MQTT** (`host:port`, username, password). The section appears when `get bridge.type` returns `mqtt`. +Веб-панель: **Настройки MQTT** → **Peer MQTT моста mesh** (`host:port`, имя пользователя, пароль). Этот раздел появляется, когда `get bridge.type` возвращает `mqtt`. -Useful checks: +Полезные проверки: ```text get bridge.type @@ -268,41 +263,41 @@ get bridge.peer.host get bridge.enabled ``` -Default admin password on dev builds is usually `password` unless you changed it. +Пароль администратора по умолчанию в dev-сборках обычно `password`, если вы его не изменили. -Packets use magic `0xC03E`, a Fletcher checksum, XOR encryption with `bridge.secret`, then publish/subscribe on `meshcore/bridge/packets`. Duplicate detection limits loops when both nodes see the same traffic. +Пакеты используют магическое значение `0xC03E`, контрольную сумму Fletcher, XOR-шифрование с `bridge.secret`, затем публикацию/подписку в `meshcore/bridge/packets`. Обнаружение дубликатов ограничивает циклы, когда оба узла видят один и тот же трафик. -More detail: +Подробнее: -- [Custom CLI — MQTT bridge settings](./eastmesh-docs/custom-cli.md) -- [Web panel — MQTT settings and bridge peer fields](./eastmesh-docs/web-panel.md) -- [Releases — track comparison](./eastmesh-docs/releases.md) +- [Пользовательский CLI — настройки MQTT-моста](./posadmesh-docs/custom-cli.md) +- [Веб-панель — настройки MQTT и поля peer моста](./posadmesh-docs/web-panel.md) +- [Релизы — сравнение треков](./posadmesh-docs/releases.md) -### Local Web Panel +### Локальная веб-панель -On supported `*_repeater_observer` ESP32 targets, the repeater can expose a local HTTPS config panel over WiFi. +На поддерживаемых таргетах ESP32 `*_repeater_observer` репитер может предоставлять локальную панель конфигурации HTTPS по WiFi. -Features include: +Возможности: -- password-gated access using the existing repeater admin password -- allowlisted CLI execution -- grouped quick actions -- light and dark themes -- optional disable via `set web off` +- доступ с проверкой пароля по существующему паролю администратора репитера +- выполнение CLI только из белого списка +- сгруппированные быстрые действия +- светлая и тёмная темы +- опциональное отключение через `set web off` -Recommended use is initial setup and occasional troubleshooting. On observer deployments, disable the panel again when finished if you want maximum MQTT heap headroom. +Рекомендуется использовать для первичной настройки и периодической диагностики. При развёртывании обсерверов отключайте панель по завершении, если нужен максимальный запас heap для MQTT. -### Companion WiFi Additions +### Дополнения Companion WiFi -`*_companion_radio_wifi` targets now support persisted WiFi rescue commands: +Таргеты `*_companion_radio_wifi` теперь поддерживают сохраняемые команды восстановления WiFi: -- open a serial monitor at `115200` baud -- reboot the device -- long-press the user button within the first 8 seconds after boot to enter `CLI Rescue` -- wait for `========= CLI Rescue =========` -- send the commands from the serial monitor +- откройте последовательный монитор на скорости `115200` бод +- перезагрузите устройство +- удерживайте пользовательскую кнопку в течение первых 8 секунд после загрузки, чтобы войти в `CLI Rescue` +- дождитесь `========= CLI Rescue =========` +- отправьте команды из последовательного монитора -These rescue commands are only available after entering `CLI Rescue`: +Эти команды восстановления доступны только после входа в `CLI Rescue`: - `get wifi.status` - `get wifi.ssid` @@ -311,58 +306,53 @@ These rescue commands are only available after entering `CLI Rescue`: - `set wifi.pwd ` - `set wifi.powersaving none|min|max` -## Active GitHub Workflows +## Сборка и релизы -- `.github/workflows/eastmesh-build-companion-wifi-firmwares.yml` -- `.github/workflows/eastmesh-build-observer-firmwares.yml` -- `.github/workflows/eastmesh-build-repeater-bridge-espnow-firmwares.yml` -- `.github/workflows/eastmesh-build-observer-espnow-firmwares.yml` -- `.github/workflows/eastmesh-build-observer-mqtt-bridge-firmwares.yml` -- `.github/workflows/eastmesh-pr-build-check.yml` -- `.github/workflows/eastmesh-push-build-check.yml` -- `.github/workflows/eastmesh-github-pages.yml` +Репозиторий живёт на . -The upstream MeshCore workflows are retained under their original filenames to reduce merge conflicts. They are not part of the EastMesh release flow and should stay disabled in GitHub Actions for this repository. +Собственные workflow GitHub Actions (сборка релизов, проверки сборки и публикация доков) удалены при переезде: релизы собираются локально через [`posadmesh-build.sh`](./posadmesh-build.sh), а теги и релизы создаются вручную в веб-интерфейсе git.meshinfo.ru. -The current release workflows intentionally focus only on: +Workflow upstream MeshCore (`.github/workflows/build-*.yml`, `firmware-builder.yml`, `pr-build-check.yml`, `run-unit-tests.yml`, `stale-bot.yml`) сохранены под исходными именами файлов для уменьшения конфликтов слияния. Они не являются частью процесса релизов PosadMesh и не используются. + +Текущие треки релизов: - `companion-wifi` - `repeater-bridge-espnow` -- `observer-eastmesh` -- `observer-eastmesh-bridge-espnow` -- `observer-eastmesh-bridge-mqtt` +- `observer-posadmesh` +- `observer-posadmesh-bridge-espnow` +- `observer-posadmesh-bridge-mqtt` -## Release Tags +## Теги релизов -Current release tags are: +Текущие теги релизов: ```bash git tag companion-wifi-v1.14.1 git tag repeater-bridge-espnow-v1.15.0 -git tag observer-eastmesh-v2026.5.1 -git tag observer-eastmesh-bridge-espnow-v2026.5.1 -git tag observer-eastmesh-bridge-mqtt-v2026.7.0 +git tag observer-posadmesh-v2026.5.1 +git tag observer-posadmesh-bridge-espnow-v2026.5.1 +git tag observer-posadmesh-bridge-mqtt-v2026.7.0 ``` -Companion WiFi uses the upstream MeshCore version in the tag. +Companion WiFi использует в теге версию upstream MeshCore. -Observer uses: +Обсервер использует: -- `OFFICIAL_MESHCORE_VERSION` from GitHub Actions variables as `FIRMWARE_VERSION` -- the EastMesh tag version as `EASTMESH_VERSION` +- `FIRMWARE_VERSION` — версия upstream MeshCore, задаётся при локальной сборке +- `POSADMESH_VERSION` — версия релиза PosadMesh из тега -See [RELEASE.md](./RELEASE.md) for the full release flow. +Полный процесс релиза см. в [RELEASE.md](./RELEASE.md). -## Documentation +## Документация -Published docs site: +Отдельный сайт документации больше не публикуется: страницы лежат в каталоге [`posadmesh-docs/`](./posadmesh-docs) и читаются прямо в веб-интерфейсе git.meshinfo.ru: -- +- -Current docs pages: +Текущие страницы документации: -- [Home](./eastmesh-docs/index.md) -- [Download and Flash Releases](./eastmesh-docs/releases.md) -- [Build Locally With uv](./eastmesh-docs/local-builds.md) -- [Custom CLI Commands](./eastmesh-docs/custom-cli.md) -- [Web Panel](./eastmesh-docs/web-panel.md) +- [Главная](./posadmesh-docs/index.md) +- [Скачивание и прошивка релизов](./posadmesh-docs/releases.md) +- [Локальная сборка с uv](./posadmesh-docs/local-builds.md) +- [Пользовательские команды CLI](./posadmesh-docs/custom-cli.md) +- [Веб-панель](./posadmesh-docs/web-panel.md) diff --git a/RELEASE.md b/RELEASE.md index 2adab788..6bc0c795 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,95 +1,94 @@ -# Releasing Firmware +# Выпуск прошивок -GitHub Actions is set up to automatically build and draft firmware releases. +Репозиторий живёт на . Автоматической сборки релизов в CI нет: прошивки собираются локально через [`posadmesh-build.sh`](./posadmesh-build.sh), а теги и релизы создаются вручную в веб-интерфейсе git.meshinfo.ru. -## Required GitHub Variable +## Версии сборки -Set the repository Actions variable `OFFICIAL_MESHCORE_VERSION` to the current upstream MeshCore release version. +- `FIRMWARE_VERSION` — версия upstream MeshCore, на которой основана сборка (например `v1.15.0`) +- `POSADMESH_VERSION` — версия релиза PosadMesh (например `v2026.5.1`) -Example: +Обе задаются переменными окружения перед запуском сборки: -- `OFFICIAL_MESHCORE_VERSION=v1.15.0` +```bash +export FIRMWARE_VERSION=v1.15.0 +export POSADMESH_VERSION=v2026.5.1 +bash posadmesh-build.sh build-firmware heltec_v4_repeater_observer +``` -This value becomes the base `FIRMWARE_VERSION` used by the release workflows. +## Теги релизов PosadMesh -## EastMesh Release Tags - -Push one or more of the following tag formats to trigger the matching firmware release workflow: +Один или несколько тегов следующих форматов помечают релиз соответствующего трека: - `companion-wifi-v1.15.0` - `repeater-bridge-espnow-v1.15.0` -- `observer-eastmesh-bridge-espnow-v2026.5.1` -- `observer-eastmesh-bridge-mqtt-v2026.7.0` -- `observer-eastmesh-v2026.5.1` +- `observer-posadmesh-bridge-espnow-v2026.5.1` +- `observer-posadmesh-bridge-mqtt-v2026.7.0` +- `observer-posadmesh-v2026.5.1` -Use the upstream MeshCore version in `companion-wifi-v1.15.0`. -Use the upstream MeshCore version in `repeater-bridge-espnow-v1.15.0`. -Use the EastMesh release version in `observer-eastmesh-bridge-espnow-v2026.5.1`. -Use the EastMesh release version in `observer-eastmesh-bridge-mqtt-v2026.7.0`. -Use the EastMesh release version in `observer-eastmesh-v2026.5.1`. +В `companion-wifi-v1.15.0` используется версия upstream MeshCore. +В `repeater-bridge-espnow-v1.15.0` используется версия upstream MeshCore. +В `observer-posadmesh-bridge-espnow-v2026.5.1` используется версия релиза PosadMesh. +В `observer-posadmesh-bridge-mqtt-v2026.7.0` используется версия релиза PosadMesh. +В `observer-posadmesh-v2026.5.1` используется версия релиза PosadMesh. -Each tag triggers a separate workflow: +Соответствие тегов и треков: -- `companion-wifi-v*` builds companion WiFi firmware -- `repeater-bridge-espnow-v*` builds repeater ESP-NOW bridge firmware -- `observer-eastmesh-bridge-espnow-v*` builds Observer ESP-NOW firmware -- `observer-eastmesh-bridge-mqtt-v*` builds Observer MQTT bridge firmware -- `observer-eastmesh-v*` builds Observer firmware +- `companion-wifi-v*` — прошивка companion WiFi +- `repeater-bridge-espnow-v*` — прошивка ESP-NOW-моста репитера +- `observer-posadmesh-bridge-espnow-v*` — прошивка обсервера с ESP-NOW +- `observer-posadmesh-bridge-mqtt-v*` — прошивка обсервера с MQTT-мостом +- `observer-posadmesh-v*` — прошивка обсервера -You can push one, or more tags on the same commit, and they will all build separately. +Сборка выполняется локально для нужного таргета; тегов на одном коммите может быть несколько. -## Resulting Firmware Version +## Итоговая версия прошивки -During the GitHub Actions build: +- `companion-wifi` использует версию из тега как `FIRMWARE_VERSION` +- `repeater-bridge-espnow` использует версию из тега как `FIRMWARE_VERSION` +- `observer-posadmesh-bridge-espnow` использует `FIRMWARE_VERSION` upstream MeshCore, а версию PosadMesh из тега — как `POSADMESH_VERSION` +- `observer-posadmesh-bridge-mqtt` использует `FIRMWARE_VERSION` upstream MeshCore, а версию PosadMesh из тега — как `POSADMESH_VERSION` +- `observer-posadmesh` использует `FIRMWARE_VERSION` upstream MeshCore, а версию PosadMesh из тега — как `POSADMESH_VERSION` -- `companion-wifi` uses the version in the tag as `FIRMWARE_VERSION` -- `repeater-bridge-espnow` uses the version in the tag as `FIRMWARE_VERSION` -- `observer-eastmesh-bridge-espnow` uses `OFFICIAL_MESHCORE_VERSION` as `FIRMWARE_VERSION` and the EastMesh version from the tag as `EASTMESH_VERSION` -- `observer-eastmesh-bridge-mqtt` uses `OFFICIAL_MESHCORE_VERSION` as `FIRMWARE_VERSION` and the EastMesh version from the tag as `EASTMESH_VERSION` -- `observer-eastmesh` uses `OFFICIAL_MESHCORE_VERSION` as `FIRMWARE_VERSION` and the EastMesh version from the tag as `EASTMESH_VERSION` - -The resulting version string depends on the workflow: +Итоговая строка версии зависит от трека: - `companion-wifi`: `v1.15.0-` - `repeater-bridge-espnow`: `v1.15.0-` -- `observer-eastmesh-bridge-espnow`: `v1.15.0-eastmesh-v2026.5.1-` -- `observer-eastmesh-bridge-mqtt`: `v1.15.0-eastmesh-v2026.7.0-` -- `observer-eastmesh`: `v1.15.0-eastmesh-v2026.5.1-` +- `observer-posadmesh-bridge-espnow`: `v1.15.0-posadmesh-v2026.5.1-` +- `observer-posadmesh-bridge-mqtt`: `v1.15.0-posadmesh-v2026.7.0-` +- `observer-posadmesh`: `v1.15.0-posadmesh-v2026.5.1-` -Example: +Пример: -- tag: `observer-eastmesh-v2026.5.1` -- repo variable: `OFFICIAL_MESHCORE_VERSION=v1.15.0` -- resulting build version: `v1.15.0-eastmesh-v2026.5.1-abcdef` +- тег: `observer-posadmesh-v2026.5.1` +- переменные: `FIRMWARE_VERSION=v1.15.0`, `POSADMESH_VERSION=v2026.5.1` +- итоговая версия сборки: `v1.15.0-posadmesh-v2026.5.1-abcdef` -## Typical Release Flow +## Типичный порядок выпуска -1. Update the `OFFICIAL_MESHCORE_VERSION` GitHub Actions variable if upstream has changed. -2. Update `release-notes.yml` on `develop` for any release entries you want included in the tagged commit. -3. Open and merge the release PR from `develop` to `main`. -4. Create the release tags you want on the target commit on `main`. -5. Push the tags. -6. Wait for the workflows to finish building. -7. Review the draft GitHub Releases. -8. Publish the release. +1. Обновите `release-notes.yml` в ветке `develop` — добавьте записи о релизе, которые должны войти в помечаемый тегом коммит. +2. Откройте и смерджите релизный PR из `develop` в `main`. +3. Соберите нужные прошивки локально с подходящими `FIRMWARE_VERSION` и `POSADMESH_VERSION`. +4. Создайте нужные теги релиза на целевом коммите в `main`. +5. Запушьте теги в git.meshinfo.ru. +6. Создайте релиз в веб-интерфейсе git.meshinfo.ru и приложите собранные файлы из `out/`. -If `release-notes.yml` should reflect the tagged firmware release in-repo, make that change before the PR to `main` so the tagged commit already contains the matching release notes. +Если `release-notes.yml` должен отражать помеченный тегом релиз прошивки в репозитории, внесите это изменение до PR в `main`, чтобы помечаемый коммит уже содержал соответствующие примечания к релизу. -Example: +Пример: ```bash -git tag companion-wifi-v1.15.0 -git tag repeater-bridge-espnow-v1.15.0 -git tag observer-eastmesh-bridge-espnow-v2026.5.1 -git tag observer-eastmesh-bridge-mqtt-v2026.7.0 -git tag observer-eastmesh-v2026.5.1 -git push origin companion-wifi-v1.15.0 repeater-bridge-espnow-v1.15.0 observer-eastmesh-bridge-espnow-v2026.5.1 observer-eastmesh-bridge-mqtt-v2026.7.0 observer-eastmesh-v2026.5.1 +export FIRMWARE_VERSION=v1.15.0 +export POSADMESH_VERSION=v2026.5.1 +bash posadmesh-build.sh build-observer-firmwares + +git tag observer-posadmesh-v2026.5.1 +git push origin observer-posadmesh-v2026.5.1 ``` -## Supported Tags +## Поддерживаемые теги - `companion-wifi-v1.15.0` - `repeater-bridge-espnow-v1.15.0` -- `observer-eastmesh-bridge-espnow-v2026.5.1` -- `observer-eastmesh-bridge-mqtt-v2026.7.0` -- `observer-eastmesh-v2026.5.1` +- `observer-posadmesh-bridge-espnow-v2026.5.1` +- `observer-posadmesh-bridge-mqtt-v2026.7.0` +- `observer-posadmesh-v2026.5.1` diff --git a/arch/esp32/CPUUsageTracker.cpp b/arch/esp32/CPUUsageTracker.cpp new file mode 100644 index 00000000..8a6432f5 --- /dev/null +++ b/arch/esp32/CPUUsageTracker.cpp @@ -0,0 +1,48 @@ +#ifdef ESP32 +#include "CPUUsageTracker.h" +#include "esp_freertos_hooks.h" + +volatile uint32_t CPUUsageTracker::s_ticks[2] = {0, 0}; +TaskHandle_t CPUUsageTracker::s_idle_handle = nullptr; + +void IRAM_ATTR CPUUsageTracker::s_tick_hook() { + s_ticks[xTaskGetCurrentTaskHandle() != s_idle_handle]++; +} + +void CPUUsageTracker::s_sample_cb(void* arg) { + static_cast(arg)->_onSample(); +} + +void CPUUsageTracker::_onSample() { + const uint32_t idle = s_ticks[0]; + const uint32_t busy = s_ticks[1]; + const uint32_t di = idle - _last_idle; + const uint32_t db = busy - _last_busy; + _last_idle = idle; + _last_busy = busy; + + const uint32_t total = di + db; + const float sample = (total > 0) ? (float)db / (float)total : 0.0f; + + const uint8_t s8 = (uint8_t)(sample * 255.0f + 0.5f); + const uint16_t last = _sma_buf[_sma_idx]; + _sma_buf[_sma_idx] = s8; + _sma_sum = (_sma_sum - last) + s8; + _sma_idx = (_sma_idx + 1) & (SMA_WINDOW - 1); +} + +void CPUUsageTracker::begin() { + s_idle_handle = xTaskGetIdleTaskHandleForCPU(0); + esp_register_freertos_tick_hook_for_cpu(s_tick_hook, 0); + + esp_timer_create_args_t args = { + .callback = s_sample_cb, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "cpu_sample" + }; + esp_timer_create(&args, &_timer); + esp_timer_start_periodic(_timer, 60000000ULL / SMA_WINDOW); // SMA_WINDOW samples per minute +} + +#endif diff --git a/arch/esp32/CPUUsageTracker.h b/arch/esp32/CPUUsageTracker.h new file mode 100644 index 00000000..15c7d7d6 --- /dev/null +++ b/arch/esp32/CPUUsageTracker.h @@ -0,0 +1,40 @@ +#pragma once +#ifdef ESP32 + +#include +#include "esp_attr.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +class CPUUsageTracker { +public: + void begin(); + + float getCore0Util() const { + return (float)_sma_sum * (1.0f / (SMA_WINDOW * 255.0f)); + } + +private: + static constexpr uint8_t SMA_WINDOW = 64; // power of 2 — enables & mask + + static volatile uint32_t s_ticks[2]; // [0] = idle ticks, [1] = busy ticks + static TaskHandle_t s_idle_handle; + + static void IRAM_ATTR s_tick_hook(); + static void s_sample_cb(void* arg); + + uint32_t _last_idle = 0; + uint32_t _last_busy = 0; + + uint8_t _sma_buf[SMA_WINDOW] = {}; + uint8_t _sma_idx = 0; + + volatile uint16_t _sma_sum = 0; + + esp_timer_handle_t _timer = nullptr; + + void _onSample(); +}; + +#endif diff --git a/arch/esp32/extra_scripts/generate_web_panel_cert.py b/arch/esp32/extra_scripts/generate_web_panel_cert.py index 010d1932..8995f394 100644 --- a/arch/esp32/extra_scripts/generate_web_panel_cert.py +++ b/arch/esp32/extra_scripts/generate_web_panel_cert.py @@ -33,7 +33,7 @@ with tempfile.TemporaryDirectory() as tmp: try: subprocess.run( - ["openssl", "ecparam", "-name", "prime256v1", "-genkey", "-noout", "-out", str(key_path)], + ["openssl", "genrsa", "-out", str(key_path), "2048"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/arch/esp32/extra_scripts/git_commit_hash.py b/arch/esp32/extra_scripts/git_commit_hash.py new file mode 100644 index 00000000..5e7fac79 --- /dev/null +++ b/arch/esp32/extra_scripts/git_commit_hash.py @@ -0,0 +1,18 @@ +Import("env") + +import subprocess + + +def git_short_hash(): + try: + return subprocess.check_output( + ["git", "rev-parse", "--short", "HEAD"], stderr=subprocess.DEVNULL + ).decode().strip() or "unknown" + except Exception: + return "unknown" + + +# Expose the current git commit hash to the firmware as GIT_COMMIT_HASH so the +# default FIRMWARE_VERSION / CLIENT_VERSION strings (used when build.sh / CI do not +# pass explicit -DFIRMWARE_VERSION flags) still identify the exact source revision. +env.Append(CPPDEFINES=[("GIT_COMMIT_HASH", env.StringifyMacro(git_short_hash()))]) diff --git a/arch/esp32/task_pinning.c b/arch/esp32/task_pinning.c new file mode 100644 index 00000000..5236173a --- /dev/null +++ b/arch/esp32/task_pinning.c @@ -0,0 +1,67 @@ +/* + * arch/esp32/task_pinning.c + * + * Pins all FreeRTOS tasks with no explicit core affinity (tskNO_AFFINITY) to + * core 0, keeping core 1 exclusively for the Arduino loop (loopTask) where + * LoRa packet processing runs. + * + * Background: + * On ESP32-S3 (dual-core), ARDUINO_RUNNING_CORE=1 pins loopTask to core 1. + * However, ESP-IDF v4 creates MQTT task with tskNO_AFFINITY, so FreeRTOS + * may schedule them on core 1 under load, preempting LoRa processing. + * + * ESP-IDF v4 provides no public API to change a task's core affinity after + * creation (vTaskCoreAffinitySet is IDF v5+ only), and esp_mqtt_client_config_t + * has no core selection field in IDF v4. The linker --wrap mechanism is the + * only way to intercept xTaskCreatePinnedToCore at the call site inside + * precompiled library code. + * + * It's intentionally do not filtered by task name — this keeps the approach + * robust against internal ESP-IDF task name changes and also catches any + * other library tasks that may happen to be created with tskNO_AFFINITY. + * Core 1 is reserved exclusively for loopTask (LoRa / application logic). + * + * Mechanism: + * -Wl,--wrap=xTaskCreatePinnedToCore redirects every call to + * xTaskCreatePinnedToCore (including the xTaskCreate macro which expands to + * it with tskNO_AFFINITY) through __wrap_xTaskCreatePinnedToCore below. + * Tasks that are already explicitly pinned to a core are passed through + * unchanged. + * + * Verified task layout after this patch: + * core 0: mqtt_task(5), httpd(2), tiT/LwIP(18), wifi(23), esp_timer(22), ipc0(24) + * core 1: loopTask(1), ipc1(24), IDLE1(0) + * + * Note: this file is compiled only for [esp32_base] targets (IDF v4). + * ESP32-C6 uses pioarduino (IDF v5) where vTaskCoreAffinitySet() is available. + */ + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +/* The real function, renamed by the linker to __real_xTaskCreatePinnedToCore. */ +extern BaseType_t __real_xTaskCreatePinnedToCore( + TaskFunction_t pvTaskCode, + const char * const pcName, + const uint32_t usStackDepth, + void * const pvParameters, + UBaseType_t uxPriority, + TaskHandle_t * const pvCreatedTask, + const BaseType_t xCoreID +); + +BaseType_t __wrap_xTaskCreatePinnedToCore( + TaskFunction_t pvTaskCode, + const char * const pcName, + const uint32_t usStackDepth, + void * const pvParameters, + UBaseType_t uxPriority, + TaskHandle_t * const pvCreatedTask, + const BaseType_t xCoreID +) { + /* Pin all tasks with no affinity to core 0. */ + BaseType_t core = (xCoreID == tskNO_AFFINITY) ? 0 : xCoreID; + return __real_xTaskCreatePinnedToCore( + pvTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pvCreatedTask, core + ); +} diff --git a/arch/esp32/tls_server_config.c b/arch/esp32/tls_server_config.c new file mode 100644 index 00000000..e51390c0 --- /dev/null +++ b/arch/esp32/tls_server_config.c @@ -0,0 +1,115 @@ +/* + * arch/esp32/tls_server_config.c + * + * Configures the TLS server via a linker-wrap of mbedtls_ssl_config_defaults(): + * + * 1. Restricts cipher suites to RSA key exchange, eliminating the ECDH + * computation that starves IDLE0 and triggers the task watchdog during + * HTTPS handshakes. + * + * 2. Enables TLS session tickets so that browsers can resume sessions + * without a full RSA handshake. + * + * Background (cipher suite restriction): + * Browsers negotiate ECDHE cipher suites by default. On ESP32-S3 the + * hardware RSA accelerator handles RSA key exchange efficiently, but + * there is no ECP hardware accelerator. ECDHE requires the server to + * compute an ephemeral key pair: ecp_precompute_comb() builds a comb + * table through many sequential ECP point doublings, each dispatched + * to the hardware bignum unit (esp_bignum.c), but the ECP layer has + * no RTOS yield points between iterations. The entire computation runs + * to completion on CPU 0 without ever resetting the task watchdog. + * + * A single handshake does not exceed the watchdog timeout on its own, + * but two consecutive handshakes (e.g. a browser retry after a failed + * attempt) accumulate enough uninterrupted runtime to starve IDLE0: + * + * E (54924) esp-tls-mbedtls: mbedtls_ssl_handshake returned -0x0050 + * E (57208) esp-tls-mbedtls: mbedtls_ssl_handshake returned -0x7280 + * E (57638) task_wdt: Task watchdog got triggered. + * E (57638) task_wdt: - IDLE0 (CPU 0) + * E (57638) task_wdt: Tasks currently running: + * E (57638) task_wdt: CPU 0: httpd + * + * The crash occurs in ecp_precompute_comb() → ecp_double_jac() → + * mbedtls_mpi_mul_mpi() during the ServerKeyExchange step. + * + * There is no sdkconfig knob accessible at runtime through + * esp_https_server. The user_cb hook fires after the handshake, too + * late to change cipher suites. The only pre-handshake intercept point + * is mbedtls_ssl_config_defaults(), called once per ssl_config init. + * + * MQTT uses MBEDTLS_SSL_IS_CLIENT; the HTTPS server uses + * MBEDTLS_SSL_IS_SERVER — this is the discriminator. + * + * Note: this file is compiled only for [esp32_base] targets (IDF v4). + * CONFIG_MBEDTLS_SSL_SESSION_TICKETS is enabled by default in the + * arduino-esp32 2.x pre-built framework, so no sdkconfig changes are needed. + */ + +#include "mbedtls/ssl.h" +#include "mbedtls/ssl_ticket.h" +#include "esp_random.h" + +/* + * RSA key exchange cipher suites only. + * Must be static — mbedTLS stores the pointer, does not copy the array. + * Terminated with 0. + */ +static const int kServerOnlyCipherSuites[] = { + MBEDTLS_TLS_RSA_WITH_AES_128_GCM_SHA256, + MBEDTLS_TLS_RSA_WITH_AES_256_GCM_SHA384, + MBEDTLS_TLS_RSA_WITH_AES_128_CBC_SHA256, + MBEDTLS_TLS_RSA_WITH_AES_256_CBC_SHA256, + 0 +}; + +/* + * Session ticket context. Must be static — mbedTLS stores the pointer for + * the lifetime of the server, analogous to kServerOnlyCipherSuites. + * Initialized once on first server start. + */ +static mbedtls_ssl_ticket_context s_ticket_ctx; +static int s_ticket_ctx_ready = 0; + +/* Thin wrapper around the ESP32 hardware RNG for mbedtls_ssl_ticket_setup(). */ +static int esp_rng(void *ctx, unsigned char *buf, size_t len) { + (void)ctx; + esp_fill_random(buf, len); + return 0; +} + +extern int __real_mbedtls_ssl_config_defaults( + mbedtls_ssl_config *conf, + int endpoint, + int transport, + int preset +); + +int __wrap_mbedtls_ssl_config_defaults( + mbedtls_ssl_config *conf, + int endpoint, + int transport, + int preset +) { + int ret = __real_mbedtls_ssl_config_defaults(conf, endpoint, transport, preset); + if (ret == 0 && endpoint == MBEDTLS_SSL_IS_SERVER) { + mbedtls_ssl_conf_ciphersuites(conf, kServerOnlyCipherSuites); + + if (!s_ticket_ctx_ready) { + mbedtls_ssl_ticket_init(&s_ticket_ctx); + int ticket_ret = mbedtls_ssl_ticket_setup(&s_ticket_ctx, esp_rng, NULL, + MBEDTLS_CIPHER_AES_256_GCM, 86400); + if (ticket_ret == 0) { + s_ticket_ctx_ready = 1; + } + } + if (s_ticket_ctx_ready) { + mbedtls_ssl_conf_session_tickets_cb(conf, + mbedtls_ssl_ticket_write, + mbedtls_ssl_ticket_parse, + &s_ticket_ctx); + } + } + return ret; +} diff --git a/boards/diy_esp32s3_n16r8_e22_back2back.json b/boards/diy_esp32s3_n16r8_e22_back2back.json new file mode 100644 index 00000000..e2997db0 --- /dev/null +++ b/boards/diy_esp32s3_n16r8_e22_back2back.json @@ -0,0 +1,44 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": [ + "wifi" + ], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "DIY_ESP32S3_N16R8_E22_Back2back", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://garden.struchkov.dev/ru/files/ESP32-S3-E22-%D1%81%D1%85%D0%B5%D0%BC%D0%B0-NanoVHF-v3.04.pdf", + "vendor": "DIY" +} diff --git a/boards/diy_esp32s3_n16r8_e22p_back2back.json b/boards/diy_esp32s3_n16r8_e22p_back2back.json new file mode 100644 index 00000000..ebd29b84 --- /dev/null +++ b/boards/diy_esp32s3_n16r8_e22p_back2back.json @@ -0,0 +1,44 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": [ + "wifi" + ], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "DIY_ESP32S3_N16R8_E22P_Back2back", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://t.me/mesh_mo_vostok/1516/5173", + "vendor": "DIY" +} diff --git a/eastmesh-docs/api.md b/eastmesh-docs/api.md deleted file mode 100644 index b9e1978c..00000000 --- a/eastmesh-docs/api.md +++ /dev/null @@ -1,410 +0,0 @@ -# Repeater Web API - -This page documents the local HTTPS API exposed by EastMesh `*_repeater_observer` builds that support the web panel. - -It is intended for: - -- lightweight automation on your local network -- dashboards or scripts that need current repeater status -- remote CLI access through the same authenticated path used by the web panel - -This is not a cloud API and not a separate backend service. The repeater firmware serves it directly. - -## Start Here - -You do not need this page for normal EastMesh use. Use the [Repeater Web Panel](./web-panel.md) if you just want to configure or check a repeater in a browser. - -Use this page when you want a local script, dashboard, or home-lab tool to talk directly to the repeater. - -The simplest useful API flow is: - -1. log in with the repeater admin password -2. save the returned token -3. send a CLI command through `/api/command` - -```bash -TOKEN=$(curl -sk -X POST https:///login --data '') - -curl -sk https:///api/command \ - -H "X-Auth-Token: $TOKEN" \ - --data 'get mqtt.status' -``` - -## Scope And Availability - -The API is available only when: - -- you are running a supported `*_repeater_observer` firmware build -- the repeater web panel is enabled and running -- you can reach the repeater over the local network -- you have authenticated with the repeater admin password - -The API is intended for trusted local-network administration. Do not expose it directly to the public internet. - -## Base URL - -Use the repeater's local HTTPS address: - -```text -https:/// -``` - -Example: - -```text -https://192.168.1.123/ -``` - -## Authentication - -The API uses the same admin password as the repeater CLI and web panel. - -1. `POST` the password to `/login` -2. store the returned session token -3. send that token in the `X-Auth-Token` header on later requests - -Example: - -```bash -TOKEN=$(curl -sk -X POST https:///login --data '') -``` - -Use the token: - -```bash -curl -sk https:///api/stats -H "X-Auth-Token: $TOKEN" -``` - -Notes: - -- the repeater uses a self-signed certificate, so most tools will need `-k` or equivalent -- if the session expires or is locked, requests return `401 Unauthorized` -- logging in again gives you a fresh token - -## Performance Guidance - -The API runs on the repeater itself, so polling frequency matters. - -If the repeater is also running two MQTT connections, avoid frequent API polling. The current EastMesh usage pattern is: - -- `60` second polling for stats -- on-demand requests for everything else - -That is the recommended baseline if you want to avoid overloading the board. Keep the request rate low, avoid bursty polling, and prefer manual refresh or event-triggered reads for heavier operations. - -Recommended practice: - -- poll `/api/stats` no more than once per minute -- avoid scraping multiple endpoints in parallel -- use on-demand calls for configuration reads and CLI actions -- close out your session when you are finished and stop polling when not actively using the data - -## Endpoints - -### `POST /login` - -Authenticate with the repeater admin password. - -Request body: - -```text - -``` - -Response: - -- plain-text session token on success -- `401` on bad password - -Example: - -```bash -curl -sk -X POST https:///login --data '' -``` - -### `GET /api/session` - -Check whether a stored session token is still valid. - -Headers: - -```text -X-Auth-Token: -``` - -Response: - -- JSON `{"authenticated":true}` on success -- `401 Unauthorized` if the token is missing or stale after lock, restart, or reflash - -### `POST /api/command` - -Run a repeater CLI command remotely. - -Headers: - -```text -X-Auth-Token: -``` - -Request body: - -```text -get wifi.status -``` - -Response: - -- plain-text CLI output -- `OK` if a command succeeds without returning text - -Example: - -```bash -curl -sk https:///api/command \ - -H "X-Auth-Token: $TOKEN" \ - --data 'get wifi.status' -``` - -### `POST /api/firmware-update` - -Upload an app firmware `.bin` through the HTTPS web panel and reboot after a successful update. - -Headers: - -```text -X-Auth-Token: -X-Firmware-MD5: <32-character md5> -Content-Type: application/octet-stream -``` - -Request body: - -```text -raw firmware .bin bytes -``` - -Response: - -- `OK` after the firmware is written and the reboot is scheduled -- `401 Unauthorized` if the token is missing or stale -- `400` or `500` if the upload or firmware verification fails - -### `GET /api/stats` - -Fetch the current summary payload used by the dedicated `/stats` page. - -Headers: - -```text -X-Auth-Token: -``` - -Example: - -```bash -curl -sk https:///api/stats \ - -H "X-Auth-Token: $TOKEN" -``` - -Notes: - -- this summary view is also what the repo's web panel requests first before loading trend series -- if `web.stats` is disabled, the endpoint returns `503 Service Unavailable` -- supported boards may also include an optional `sensors` object in the summary payload for current GPS and environmental telemetry -- the `core` object includes raw `battery_mv`, board-reported `battery_pct` when available, a UI-ready `battery_display_pct`, and board-specific `battery_min_mv` / `battery_max_mv` range hints used by `/stats` when the board does not expose its own battery percentage - -### `GET /api/stats?series=` - -Fetch one trend series. - -Supported series: - -- `battery` -- `memory` -- `signal` -- `noise_floor` -- `packets` -- `voltage` -- `sensor_temp` -- `humidity` -- `pressure` -- `pressure_altitude` -- `mcu_temp` -- `gps_altitude` -- `gps_satellites` - -Example: - -```bash -curl -sk "https:///api/stats?series=memory" \ - -H "X-Auth-Token: $TOKEN" -``` - -Notes: - -- use `?series=battery`, not just `?series` -- the built-in web panel loads these series sequentially rather than all at once to keep board memory pressure lower -- environment series are included only when the board reports those readings; if a series has no captured points yet, it returns an empty `points` array and `current:null` - -### `GET /api/stats?view=legacy` - -Fetch the older bundle-style stats payload. - -Example: - -```bash -curl -sk "https:///api/stats?view=legacy" \ - -H "X-Auth-Token: $TOKEN" -``` - -This exists for compatibility and troubleshooting. For new integrations, prefer the summary endpoint plus specific `series` requests. - -## Common Use Cases - -### 1. Remote CLI Access - -`/api/command` is the most flexible endpoint. It lets you run the same CLI commands accepted by the repeater. - -Examples: - -- `get wifi.status` -- `get mqtt.status` -- `get web.status` -- `get web.stats.status` -- `get repeat` -- `get radio` - -This is useful for: - -- remote diagnostics from a laptop or phone -- simple scripts that collect operational state -- admin tools that want to reuse existing CLI behaviour instead of adding new firmware endpoints - -Example: - -```bash -curl -sk https:///api/command \ - -H "X-Auth-Token: $TOKEN" \ - --data 'get mqtt.status' -``` - -### 2. Build A Lightweight Status Dashboard - -Use `/api/stats` for summary information and one `series` call at a time for trend lines. - -Recommended pattern: - -1. fetch `/api/stats` -2. render current service state and summary fields -3. fetch one trend series only when needed -4. refresh at `60` second intervals, or slower if the repeater is busy - -This is the same basic pattern used by the built-in `/stats` page. - -### 3. Reuse The API For Quick Health Checks - -Because `/api/command` returns CLI output directly, it works well for small operational checks in scripts or home-lab monitoring. - -Examples: - -- confirm the repeater still has Wi-Fi -- check MQTT broker connection state -- verify that the web panel is enabled before attempting stats reads -- confirm current radio settings before applying changes - -Example: - -```bash -curl -sk https:///api/command \ - -H "X-Auth-Token: $TOKEN" \ - --data 'get web.status' -``` - -### 4. Remote Admin Actions - -The web panel also uses `/api/command` for operator actions, not just read-only queries. - -Examples: - -- `advert` -- `reboot` -- `start ota` -- `time ` -- `time.force ` - -These are powerful commands. Treat them the same way you would treat direct serial CLI access. - -Example: - -```bash -curl -sk https:///api/command \ - -H "X-Auth-Token: $TOKEN" \ - --data 'advert' -``` - -### 5. Remote Configuration Helpers - -The web panel saves settings by generating CLI commands and sending them through `/api/command`. - -That means your own tools can do the same for EastMesh-specific settings such as: - -- repeater identity fields -- owner info -- MQTT broker toggles -- EastMesh MQTT owner metadata -- radio settings supported by the repeater CLI - -This is a practical way to automate setup while preserving existing CLI semantics. - -## Example Script - -This shell example logs in, fetches summary stats, fetches one trend series, and runs a CLI command: - -```bash -#!/usr/bin/env bash -set -euo pipefail - -BASE_URL="https://192.168.1.123" -PASSWORD="your-admin-password" - -TOKEN=$(curl -sk -X POST "$BASE_URL/login" --data "$PASSWORD") - -echo "Summary:" -curl -sk "$BASE_URL/api/stats" \ - -H "X-Auth-Token: $TOKEN" - -echo -echo "Memory trend:" -curl -sk "$BASE_URL/api/stats?series=memory" \ - -H "X-Auth-Token: $TOKEN" - -echo -echo "MQTT status:" -curl -sk "$BASE_URL/api/command" \ - -H "X-Auth-Token: $TOKEN" \ - --data 'get mqtt.status' -``` - -## Error Cases - -Common responses: - -- `401 Unauthorized`: missing or expired token -- `503 Service Unavailable`: stats are disabled -- `404 No stats data`: requested stats payload could not be built -- `400 Bad request`: malformed login or command request body - -If stats requests fail: - -1. confirm the web panel is enabled -2. confirm `web.stats` is enabled -3. confirm the session token is still valid -4. reduce polling frequency if the board is under memory pressure - -## Practical Recommendations - -- prefer `/api/command` when you need exact CLI parity -- prefer `/api/stats` for dashboards and trend views -- keep polling conservative, especially on repeaters with two active MQTT connections -- if you are finished with troubleshooting, consider disabling the web panel with `set web off` to maximise heap headroom on constrained boards diff --git a/eastmesh-docs/boards.md b/eastmesh-docs/boards.md deleted file mode 100644 index fbf7b654..00000000 --- a/eastmesh-docs/boards.md +++ /dev/null @@ -1,196 +0,0 @@ -# Board Comparison - -This page is meant to help you choose a board, not just list every technical detail. - -## Start Here - -If you are new to MeshCore hardware and just want a sensible starting point, first decide whether the device is a companion or a repeater. - -For companions, think Wi-Fi-connected automation endpoint: - -- use companion Wi-Fi builds when another system, such as Home Assistant, RemoteTerm, or your own tooling, needs to connect to MeshCore over Wi-Fi -- these are usually not hands-on daily devices, so a screen is optional rather than a must-have -- headless or simple boards are often fine when the companion will sit near power and be managed from another app or automation system -- check the companion tables below when you do want a display, GPS, or a more complete board package - -For MQTT/web repeaters, think Wi-Fi, broker visibility, and enough headroom for the local web panel: - -- common starting points are `Heltec_v3` and `heltec_v4` -- `heltec_v4_repeater_observer` is a strong all-round observer choice with GPS, PSRAM, and 16 MB flash -- choose boards with more flash and PSRAM when you want MQTT plus the local web panel, stats, or more room for future features -- screens are not a major requirement because observers have the local web panel for setup and troubleshooting -- headless repeaters are often the cleanest choice when the device will live in a roof, box, or fixed install and you will configure it from the web panel, serial console, or app - -For bridge repeaters, think local radio linking between nearby repeaters: - -- use bridge firmware when two local repeaters need to exchange traffic across different MeshCore radio configs, such as `Australia (Narrow)` and `Australia (Mid)` -- pure ESP-NOW bridge repeaters do not need MQTT, the EastMesh web panel, or a screen -- prioritise stable power, suitable antennas, and placement over display features -- use `observer-eastmesh-bridge-espnow` only when the same repeater also needs MQTT uplink and a local ESP-NOW bridge; otherwise `repeater-bridge-espnow` keeps the role simpler - -Other notes: - -- screens matter more for companions or bench/field devices than fixed repeaters -- if you do want an onboard screen people will actually use, prefer TFT boards -- if you want a low-power status screen, prefer e-paper boards -- if location-aware or mobile use matters, prefer GPS-capable boards such as the T-Beam family, `heltec_tracker_v2`, `Heltec_v3`, `heltec_v4`, or `ThinkNode_M5` -- if you want the most conservative, older radio family choices, the `SX1276` boards are `LilyGo_TLora_V2_1_1_6`, `Tbeam_SX1276`, and `Heltec_v2` - -## About The Tables - -The tables below are built from the repo's PlatformIO board metadata and variant build flags. - -- `Target` is the short board name used in this comparison page. Release filenames and local build commands still use the full env names such as `heltec_v4_repeater_observer`. -- `MCU` shows the chip family only. Actual runtime clock can vary by env and board configuration. -- `RAM` is the MCU's built-in RAM. -- `PSRAM` is extra memory on some boards. More PSRAM usually means more headroom for UI, MQTT, and future features. -- `GPS` uses `✅` when present and is blank when absent. -- `SD` uses `✅` when the board is currently known to support the SD-backed archive path in EastMesh, `🧪` when the hardware likely supports TF/microSD but the board-specific integration still needs validation, and is blank when there is no current SD/archive support note. - -## Track Notes - -- `companion-wifi` boards are for app-connected companion devices. -- `observer-eastmesh` boards are for Wi-Fi repeaters that publish to MQTT. -- `repeater-bridge-espnow` and `observer-eastmesh-bridge-espnow` boards are for local ESP-NOW bridge use between nearby repeaters on different MeshCore radio configs. - -Bridge firmware is not MQTT-over-WAN or VPN bridging. Use it when two local repeaters need to exchange traffic across radio configs such as `Australia (Narrow)` and `Australia (Mid)`. - -## Additional Signals - -If you are deciding between otherwise similar boards, these target-level settings often matter more than flash alone. - -- `CPU cfg` is the runtime clock MeshCore sets for that target. `default` means MeshCore does not override the board's default clock. -- `TX cfg` is MeshCore's configured `LORA_TX_POWER`, not guaranteed antenna output. Boards with a PA or RF front-end may radiate much more than the configured value suggests. -- `Power` is how the target exposes power in the repo: `ADC batt`, `PMU`, `custom 2S`, `fixed/ext`, or `none`. -- `Extras` lists notable RF or peripheral extras exposed by the target, such as `PA/FEM`, `RF switch`, `BME280`, `NeoPixel`, or `ext ant`. - -| Target | CPU cfg | TX cfg | Power | Extras | -| ------------------------ | ------- | ------------ | --------- | --------- | -| Ebyte_EoRa-S3 | default | 22 dBm cfg | ADC batt | | -| Generic_E22_sx1262 | default | 22 dBm cfg | ADC batt | RF switch | -| Generic_E22_sx1268 | default | 22 dBm cfg | ADC batt | RF switch | -| Heltec_E213 | default | 22 dBm cfg | ADC batt | | -| Heltec_E290 | default | 22 dBm cfg | ADC batt | | -| Heltec_T190 | default | 22 dBm cfg | ADC batt | | -| heltec_tracker_v2 | 160 MHz | 9 dBm cfg\* | ADC batt | PA/FEM | -| Heltec_v2 | default | 20 dBm cfg | ADC batt | | -| Heltec_v3 | 80 MHz | 22 dBm cfg | ADC batt | | -| heltec_v4 | 80 MHz | 10 dBm cfg\* | ADC batt | PA/FEM | -| heltec_v4_tft | 80 MHz | 10 dBm cfg\* | ADC batt | PA/FEM | -| Heltec_Wireless_Paper | default | 22 dBm cfg | ADC batt | | -| Heltec_Wireless_Tracker | 80 MHz | 22 dBm cfg | none | | -| Heltec_WSL3 | 80 MHz | 22 dBm cfg | ADC batt | | -| LilyGo_T3S3_sx1262 | default | 22 dBm cfg | ADC batt | | -| LilyGo_T3S3_sx1276 | default | 20 dBm cfg | ADC batt | RF switch | -| LilyGo_TBeam_1W | default | 22 dBm cfg\* | custom 2S | PA/FEM | -| LilyGo_TDeck | default | 22 dBm cfg | ADC batt | | -| LilyGo_Tlora_C6 | default | 22 dBm cfg | none | RF switch | -| LilyGo_TLora_V2_1_1_6 | default | 20 dBm cfg | ADC batt | | -| M5Stack_Unit_C6L | default | 22 dBm cfg | none | RF switch | -| Meshadventurer_sx1262 | default | 22 dBm cfg | ADC batt | RF switch | -| Meshadventurer_sx1268 | default | 22 dBm cfg | ADC batt | RF switch | -| Meshimi | default | 22 dBm cfg | none | ext ant | -| nibble_screen_connect | default | 22 dBm cfg | none | NeoPixel | -| RAK_3112 | 80 MHz | 22 dBm cfg | ADC batt | | -| Station_G2 | default | 7 dBm cfg\* | fixed/ext | PA/FEM | -| Station_G2_logging | default | 7 dBm cfg\* | fixed/ext | PA/FEM | -| T_Beam_S3_Supreme_SX1262 | default | 22 dBm cfg | PMU | BME280 | -| Tbeam_SX1262 | default | 22 dBm cfg | PMU | | -| Tbeam_SX1276 | default | 20 dBm cfg | PMU | | -| ThinkNode_M2 | default | 22 dBm cfg | ADC batt | | -| ThinkNode_M5 | default | 22 dBm cfg | ADC batt | | -| WHY2025_badge | default | 22 dBm cfg | none | | -| Xiao_C6 | default | 22 dBm cfg | none | RF switch | -| Xiao_S3_LoRa2021F33-2G4 | default | 22 dBm cfg | ADC batt | RF switch | -| Xiao_S3_WIO | default | 22 dBm cfg | none | RF switch | - -\* These targets use a PA or RF front-end, so effective output can be much higher than the configured `LORA_TX_POWER`. - -## `repeater_observer` Boards - -This table includes all observer targets currently defined in `variants/eastmesh/platformio.ini`. - -| Target | MCU | RAM | PSRAM | Flash | LoRa | Display | GPS | SD | -| ------------------------ | ------- | ------ | ----- | ----- | ------ | --------------- | --- | --- | -| Ebyte_EoRa-S3 | ESP32S3 | 512 KB | 2 MB | 4 MB | SX1262 | OLED (SSD1306) | | | -| Generic_E22_sx1262 | ESP32 | 520 KB | No | 4 MB | SX1262 | None | | | -| Generic_E22_sx1268 | ESP32 | 520 KB | No | 4 MB | SX1268 | None | | | -| Heltec_E213 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | E-paper (2.13") | | | -| Heltec_E290 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | E-paper (2.9") | | | -| Heltec_T190 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | TFT (ST7789) | | | -| heltec_tracker_v2 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | TFT (ST7735) | ✅ | | -| Heltec_v2 | ESP32 | 520 KB | No | 8 MB | SX1276 | OLED (SSD1306) | | | -| Heltec_v3 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | OLED (SSD1306) | ✅ | | -| heltec_v4 | ESP32S3 | 512 KB | 2 MB | 16 MB | SX1262 | OLED (SSD1306) | ✅ | | -| heltec_v4_tft | ESP32S3 | 512 KB | 2 MB | 16 MB | SX1262 | TFT (ST7789) | ✅ | | -| Heltec_Wireless_Paper | ESP32S3 | 512 KB | No | 8 MB | SX1262 | E-paper (2.13") | | | -| Heltec_Wireless_Tracker | ESP32S3 | 512 KB | No | 8 MB | SX1262 | TFT (ST7735) | ✅ | | -| Heltec_WSL3 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | None | ✅ | | -| LilyGo_T3S3_sx1262 | ESP32S3 | 512 KB | 2 MB | 4 MB | SX1262 | OLED (SSD1306) | | 🧪 | -| LilyGo_T3S3_sx1276 | ESP32S3 | 512 KB | 2 MB | 4 MB | SX1276 | OLED (SSD1306) | | 🧪 | -| LilyGo_TBeam_1W | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | OLED (SH1106) | ✅ | ✅ | -| LilyGo_TDeck | ESP32S3 | 512 KB | No | 16 MB | SX1262 | TFT (ST7789) | ✅ | 🧪 | -| LilyGo_Tlora_C6 | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | | | -| M5Stack_Unit_C6L | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | ✅ | | -| Meshadventurer_sx1262 | ESP32 | 520 KB | No | 4 MB | SX1262 | OLED (SSD1306) | ✅ | | -| Meshadventurer_sx1268 | ESP32 | 520 KB | No | 4 MB | SX1268 | OLED (SSD1306) | ✅ | | -| Meshimi | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | | | -| nibble_screen_connect | ESP32S3 | 512 KB | No | 4 MB | SX1262 | OLED (SSD1306) | | | -| RAK_3112 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | None | ✅ | | -| Station_G2 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | OLED (SH1106) | ✅ | | -| Station_G2_logging | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | OLED (SH1106) | ✅ | | -| T_Beam_S3_Supreme_SX1262 | ESP32S3 | 512 KB | 8 MB | 8 MB | SX1262 | OLED (SH1106) | ✅ | ✅ | -| Tbeam_SX1262 | ESP32 | 520 KB | No | 4 MB | SX1262 | OLED (SSD1306) | ✅ | | -| Tbeam_SX1276 | ESP32 | 520 KB | No | 4 MB | SX1276 | OLED (SSD1306) | ✅ | | -| ThinkNode_M2 | ESP32S3 | 512 KB | No | 4 MB | SX1262 | OLED (SH1106) | | | -| ThinkNode_M5 | ESP32S3 | 512 KB | No | 4 MB | SX1262 | E-paper (GxEPD) | ✅ | | -| WHY2025_badge | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | | | -| Xiao_C6 | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | | | -| Xiao_S3_LoRa2021F33-2G4 | ESP32S3 | 512 KB | No | 8 MB | LR2021 | None | | | -| Xiao_S3_WIO | ESP32S3 | 512 KB | No | 8 MB | SX1262 | None | ✅ | | - -SD notes: - -- `✅` means the board is currently known to work with the SD-backed archive path in EastMesh. -- `🧪` means the board hardware likely supports TF/microSD, but the EastMesh board-specific integration still needs validation before it should be treated as supported. -- For the current stats archive, a `4 GB` `FAT32` microSD card is more than sufficient. Larger cards are also supported. - -## `companion_radio_wifi` Boards With A Display - -These are the Wi-Fi companion targets that have a display configured for local status, setup help, or occasional direct interaction. - -| Target | MCU | RAM | PSRAM | Flash | LoRa | Display | GPS | -| ------------------------ | ------- | ------ | ----- | ----- | ------ | --------------- | --- | -| Heltec_v2 | ESP32 | 520 KB | No | 8 MB | SX1276 | OLED (SSD1306) | | -| Heltec_v3 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | OLED (SSD1306) | ✅ | -| heltec_tracker_v2 | ESP32S3 | 512 KB | No | 8MB | SX1262 | TFT (ST7735) | ✅ | -| heltec_v4 | ESP32S3 | 512 KB | 2 MB | 16MB | SX1262 | OLED (SSD1306) | ✅ | -| heltec_v4_tft | ESP32S3 | 512 KB | 2 MB | 16MB | SX1262 | TFT (ST7789) | ✅ | -| LilyGo_TBeam_1W | ESP32S3 | 512 KB | 8 MB | 16MB | SX1262 | OLED (SH1106) | ✅ | -| LilyGo_TLora_V2_1_1_6 | ESP32 | 520 KB | No | 4 MB | SX1276 | OLED (SSD1306) | ✅ | -| nibble_screen_connect | ESP32S3 | 512 KB | No | 4MB | SX1262 | OLED (SSD1306) | | -| Station_G2 | ESP32S3 | 512 KB | 8 MB | 16MB | SX1262 | OLED (SH1106) | ✅ | -| T_Beam_S3_Supreme_SX1262 | ESP32S3 | 512 KB | 8 MB | 8MB | SX1262 | OLED (SH1106) | ✅ | -| ThinkNode_M2 | ESP32S3 | 512 KB | No | 4MB | SX1262 | OLED (SH1106) | | -| ThinkNode_M5 | ESP32S3 | 512 KB | No | 4MB | SX1262 | E-paper (GxEPD) | ✅ | - -## `companion_radio_wifi` Headless Boards - -These are the Wi-Fi companion targets that rely on the companion app as the primary interface. For many users, this is the best everyday option. - -| Target | MCU | RAM | PSRAM | Flash | LoRa | GPS | -| ----------------------- | ------- | ------ | ----- | ----- | ------ | --- | -| Heltec_WSL3 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | ✅ | -| RAK_3112 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | ✅ | -| Xiao_S3_LoRa2021F33-2G4 | ESP32S3 | 512 KB | No | 8 MB | LR2021 | | -| Xiao_S3_WIO | ESP32S3 | 512 KB | No | 8 MB | SX1262 | ✅ | - -## Practical Picks - -- Best all-round observer with screen and overall headroom: `heltec_v4_repeater_observer`, `heltec_v4_tft_repeater_observer`, `Station_G2_repeater_observer`, `LilyGo_TBeam_1W_repeater_observer`. -- Best observer if you want a simple headless install: `RAK_3112_repeater_observer`, `Generic_E22_sx1262_repeater_observer`, `Meshimi_repeater_observer`, `Xiao_C6_repeater_observer`. -- Best low-power display observer builds: `Heltec_E213_repeater_observer`, `Heltec_E290_repeater_observer`, `Heltec_Wireless_Paper_repeater_observer`, `ThinkNode_M5_Repeater_observer`. -- Best companion choices if you want the richest local UI: `heltec_v4_tft_companion_radio_wifi` and `heltec_tracker_v2_companion_radio_wifi`. -- Best companion choices if you want maximum memory headroom: `T_Beam_S3_Supreme_SX1262_companion_radio_wifi`, `Station_G2_companion_radio_wifi`, `LilyGo_TBeam_1W_companion_radio_wifi`. -- Best companion choices if you are happy with an app-first, mostly headless setup: `RAK_3112_companion_radio_wifi`, `Xiao_S3_WIO_companion_radio_wifi`, `Station_G2_companion_radio_wifi`. -- Best companion choices if you prefer OLED over TFT: `T_Beam_S3_Supreme_SX1262_companion_radio_wifi`, `heltec_v4_companion_radio_wifi`, `LilyGo_TBeam_1W_companion_radio_wifi`. diff --git a/eastmesh-docs/custom-cli.md b/eastmesh-docs/custom-cli.md deleted file mode 100644 index 37b3266e..00000000 --- a/eastmesh-docs/custom-cli.md +++ /dev/null @@ -1,326 +0,0 @@ -# Custom CLI Commands - -This page covers the EastMesh-specific CLI commands added in this repository. - -It does not try to repeat the full upstream MeshCore CLI surface. - -## Start Here - -If you are doing first-time setup, these are the commands most users need before anything else: - -```text -set wifi.ssid -set wifi.pwd -get wifi.status -set mqtt.iata -get mqtt.status -``` - -For observers with the local web panel, these are also useful: - -```text -set web on -get web.status -set web off -``` - -Use `set web on` while setting up or troubleshooting, then use `set web off` when a fixed observer needs maximum memory headroom. - -## Observer Commands - -These commands are available on `*_repeater_observer` firmware targets. - -No-argument `get` commands must be entered exactly as shown. - -### MQTT Status And Routing - -- `get mqtt.status`: shows Wi-Fi, NTP, IATA, endpoint status, custom endpoint transport, status publishing state, TX state, and `leaked:` (MQTT clients intentionally left unfreed after a heap-integrity failure at teardown; should stay `0`). -- `get mqtt.statuscfg`: shows whether periodic status messages are enabled as a simple `on` or `off` value. Most users can just use `get mqtt.status`. -- `get mqtt.client_version`: shows the MQTT `client_version` string published by the repeater. -- `get mqtt.client_env`: shows the PlatformIO env used to build the repeater firmware. -- `get mqtt.iata`: shows the IATA/location code used in MQTT topics. -- `set mqtt.iata `: sets the IATA/location code, for example `MEL`. -- `set mqtt.iata UNSET`: marks MQTT IATA as not configured yet. While it is `UNSET`, enabled MQTT brokers stay disconnected until a real code is saved. - -### MQTT Identity - -- `get mqtt.owner`: shows the configured owner public key. -- `set mqtt.owner <64-hex-char-public-key>`: sets the owner public key used in JWT metadata. -- `mqtt.owner <64-hex-char-public-key>`: shorthand for setting the owner public key. -- `get mqtt.email`: shows the configured owner email. -- `set mqtt.email `: sets the owner email used in JWT metadata. -- `mqtt.email `: shorthand for setting the owner email. - -### MQTT Message Controls - -- `get mqtt.packets`: shows whether packet messages are published. -- `set mqtt.packets on|off`: enables or disables packet publishing. -- `get mqtt.raw`: shows whether raw packet payloads are published. -- `set mqtt.raw on|off`: enables or disables the separate `raw` MQTT topic. -- `set mqtt.status on|off`: enables or disables periodic MQTT status publishing. -- `get mqtt.tx`: shows whether TX packets are included. -- `set mqtt.tx on|off`: enables or disables TX packet publishing. - -### MQTT Endpoints - -- `get mqtt.eastmesh-au` -- `set mqtt.eastmesh-au on|off` -- `get mqtt.meshmapper` -- `set mqtt.meshmapper on|off` -- `get mqtt.waev` -- `set mqtt.waev on|off` -- `get mqtt.letsmesh-eu` (retired) -- `set mqtt.letsmesh-eu on|off` (retired) -- `get mqtt.letsmesh-us` (retired) -- `set mqtt.letsmesh-us on|off` (retired) -- `get mqtt.custom` -- `set mqtt.custom on|off` -- `get mqtt.custom.host` -- `set mqtt.custom.host ` -- `get mqtt.custom.port` -- `set mqtt.custom.port ` -- `get mqtt.custom.transport`: shows `tcp` or `wss`. -- `set mqtt.custom.transport tcp|wss` -- `get mqtt.custom.username` -- `set mqtt.custom.username ` -- `get mqtt.custom.password`: shows `set` when a custom password is configured. -- `set mqtt.custom.password ` - -Notes: - -- new observer installs default `mqtt.iata` to `UNSET` -- a maximum of two MQTT brokers can be enabled at once -- `meshmapper` is the curated global broker (`wss://mqtt.meshmapper.net:443/mqtt`); like `eastmesh-au` it uses WSS, verified TLS, and MeshCore JWT auth with the broker host as the token audience -- `waev` is a curated broker (`wss://mqtt.waev.app:443/mqtt`); like `eastmesh-au` it uses WSS, verified TLS, and MeshCore JWT auth with the broker host as the token audience, but enforces a 1-hour auth token lifetime (the firmware mints a shorter-lived token for it automatically) -- `letsmesh-eu` and `letsmesh-us` are retired (LetsMesh is no longer maintained) and off by default; any saved selections are cleared once on upgrade, though the endpoints stay selectable for legacy use -- if `mqtt.iata` is `UNSET`, enabled MQTT brokers will not connect -- custom MQTT uses the configured username and password, not JWT authentication -- custom MQTT defaults to TCP; set `mqtt.custom.transport wss` for MQTT over secure WebSockets using the fixed `/mqtt` websocket path and the ESP-IDF x509 root CA bundle -- `get mqtt.status` reports the two enabled broker slots as `p::` (primary) and `s::` (secondary), or `-` when a slot is empty; custom appears as `custom::` (for example `p:custom:wss:up`); `conn` means connecting and `up` means connected -- custom MQTT uses the same `meshcore///` topics as the curated brokers -- turning off a connected broker publishes a retained MQTT status update with `"status":"offline"` before the client disconnects -- changing `mqtt.iata` away from a configured value also publishes retained offline status to the old status topic, restarts connected broker clients, and reconnects under the new topic path - -Legacy dotted aliases are also accepted: - -- `mqtt.eastmesh.au` -- `mqtt.letsmesh.eu` -- `mqtt.letsmesh.us` - -### Wi-Fi Settings For Observers - -- `get wifi.status`: shows SSID, connection state, raw Wi-Fi status code, IP, channel, and signal when connected, plus gateway health (`gw:ok|lost`) and the watchdog reconnect count (`wd:`). -- `get wifi.ssid`: shows the configured Wi-Fi SSID. -- `set wifi.ssid `: sets the Wi-Fi SSID. -- `set wifi.pwd `: sets the Wi-Fi password. -- `get wifi.powersaving`: shows the current Wi-Fi power save mode. -- `set wifi.powersaving none|min|max`: sets Wi-Fi power saving mode. -- `wifi reconnect`: drops the current association and rejoins from a full channel scan. Use when a node reports connected but is unreachable over the network. - -Observers also run a connectivity watchdog: while Wi-Fi reports connected, the node ARP-probes its gateway every 30 seconds. If the gateway stays silent for 3 minutes (for example, an AP that keeps beaconing after losing its wired uplink), the node forces a full reconnect on its own, backing off up to 48 minutes between attempts while the outage persists. `wd:` in `get wifi.status` counts these forced reconnects since boot. - -### NTP Settings For Observers - -- `get ntp.server1`: shows the primary NTP server. -- `get ntp.server2`: shows the secondary NTP server. -- `get ntp.server3`: shows the tertiary NTP server. -- `set ntp.server1 `: sets the primary NTP server and restarts time sync. -- `set ntp.server2 `: sets the secondary NTP server and restarts time sync. -- `set ntp.server3 `: sets the tertiary NTP server and restarts time sync. - -Default servers are `au.pool.ntp.org`, `time.google.com`, and `time.cloudflare.com`. - -### ESP-NOW Bridge Settings For Observer ESP-NOW Builds - -These commands are available on `*_repeater_observer_espnow` firmware targets that use the ESP-NOW bridge transport. - -Bridge commands are for local ESP-NOW bridge use between nearby repeaters, such as linking repeaters on `Australia (Narrow)` and `Australia (Mid)`. They are not MQTT-over-WAN, VPN, or internet bridge controls. - -- `get bridge.channel`: shows the configured ESP-NOW bridge channel. -- `set bridge.channel `: sets the ESP-NOW bridge channel and restarts the bridge. Use a value from `1` to `14`. -- `get bridge.secret`: shows the configured ESP-NOW bridge secret. -- `set bridge.secret `: sets the shared ESP-NOW bridge secret and restarts the bridge. - -After running `set bridge.channel`, expect the bridge and web panel connection to drop briefly while the radio restarts. On current observer ESP-NOW builds, this can look like the board rebooted. - -For `*_repeater_observer_espnow` builds that are connected to Wi-Fi, the ESP-NOW bridge channel must match the active 2.4 GHz Wi-Fi channel: - -1. Run `get wifi.status`. -2. Read the `channel:` value from the connected Wi-Fi status. -3. Run `get bridge.channel`. -4. If the values differ, run `set bridge.channel ` using the Wi-Fi channel value. -5. Use the same `bridge.channel` and `bridge.secret` on every ESP-NOW bridge node that should talk together. - -Example: - -```text -> get wifi.status -> ssid:EastMesh-IoT status:connected code:3 state:connected ip:192.168.1.50 channel:6 rssi:-61 quality:78% signal:good gw:ok wd:0 -> get bridge.channel -> 1 -> set bridge.channel 6 -OK -``` - -### Web Panel Controls - -- `get web` -- `get web.status`: shows whether the local HTTPS panel is available, plus `heals:` (times the panel was restarted because internal heap was too fragmented for a TLS handshake) and `deferred:` (start attempts postponed until heap headroom recovered). -- `get web.stats.status`: shows whether the dedicated stats page and history subsystem are enabled, whether recent history is active, whether PSRAM-backed history is available, and whether the SD-backed archive is mounted. When enabled, the history capture now covers supported environment telemetry too, not just the original battery/radio series. GPS-active boards also record per-minute satellites samples for the `/stats` history view. If archive access drops while stats remain enabled, the repeater retries the SD mount periodically. -- At boot, archive-backed stats restore uses bounded reads of the latest summary, events, and neighbour snapshots so malformed or unexpectedly large archive files do not delay MQTT or web startup. -- `set web on|off` -- `set.web on|off`: enables or disables the local HTTPS panel. -- `set web.stats on|off` -- `set.web.stats on|off`: enables or disables the dedicated `/stats` page and historical stats collection. -- `purge sd`: deletes files and directories from the mounted SD card archive, then recreates the empty `/stats` archive directory so runtime stats capture can continue. It does not erase the internal repeater filesystem or stored settings. - -### Runtime Diagnostics - -- `memory`: shows current heap and PSRAM usage. -- `stats-core`: shows battery, uptime, sticky error count, and outbound queue depth. -- `stats-radio`: shows radio noise floor, last RSSI, last SNR, and TX/RX airtime. -- `stats-packets`: shows packet receive/send totals, flood/direct breakdown, and receive errors. - -> If `noise_floor` reports `0`, check `get agc.reset.interval`; if it is not `0`, try `set agc.reset.interval 0` and test again. - -### Flood Forwarding Limit - -- `get flood.max.unscoped`: shows the hop limit for unscoped flood packets. -- `set flood.max.unscoped <0-64>`: sets the hop limit for unscoped flood packets. -- `get flood.max.advert`: shows the hop limit for flooded advert packets. -- `set flood.max.advert <0-64>`: sets the hop limit for flooded advert packets. - -Observer builds default `flood.max.unscoped` to `64`. Lower values can limit how far unscoped flood traffic is repeated while leaving scoped/region flood forwarding controlled by `flood.max`. - -### Board Battery Reporting - -- On observer builds, background battery sampling used for MQTT/status history is rate-limited to about once per minute. Explicit status and telemetry requests still refresh the reading immediately. - -### T-Beam 1W Fan Control - -These commands are only available on `LilyGo_TBeam_1W_*` repeater builds. - -- `get fan`: shows the current fan mode, current fan state, and the last NTC-based board temperature when available. -- `set fan auto`: returns the fan to automatic control and persists that mode across reboot. -- `set fan on`: forces the fan on and persists that mode across reboot. -- `set fan off`: forces the fan off and persists that mode across reboot. -- `set fan timeout `: changes the automatic post-TX hold window in seconds and persists it across reboot, for example `set fan timeout 45s`. - -Auto mode behaviour: - -- forces the fan on during TX and keeps it on for the configured timeout afterward -- otherwise turns the fan on at `48C` -- turns it back off at `42C` -- keeps the fan on if the NTC reading is unavailable - -Notes: - -- default repeater fan mode is `auto` -- default post-TX timeout is `30s` -- fan mode and timeout are stored in repeater prefs and survive reboot -- only `LilyGo_TBeam_1W_*` repeater builds use these persisted fan settings -- accepted range is `0s` to `600s` - -## Web Panel CLI Access - -When the repeater web panel is enabled and you are authenticated, the browser CLI panel can run the same CLI commands accepted by the repeater. - -Notes: - -- the panel still uses the repeater admin password for access -- commands run with the same care as if you typed them into the repeater CLI directly -- this is intended for local admin use on a trusted network -- `start ota` releases the local HTTP redirect listener on port `80` so the OTA HTTP listener can take over without stopping the rest of the repeater services, regardless of whether the command is run from the web panel, serial CLI, or a remote companion/app CLI session -- if the old redirect listener has not fully released port `80`, the OTA listener retries for up to about 30 seconds before giving up -- the web `Purge SD` button runs `purge sd` after browser confirmation -- `start ota` uses the repeater's existing Wi-Fi address when already connected, or starts the `MeshCore-OTA` access point when Wi-Fi is not connected -- the `/app` Regions shortcut runs the existing MeshCore region commands in sequence: `region put au`, `region put au-STATE`, `region allowf au`, `region allowf au-STATE`, then `region save` - -## Companion Wi-Fi Rescue Commands - -These commands are available in the serial rescue CLI for `*_companion_radio_wifi` builds. - -To enter `CLI Rescue`: - -- open a serial monitor at `115200` baud -- reboot the device -- long-press the user button within the first 8 seconds after boot -- wait for `========= CLI Rescue =========` - -- `get wifi.status`: shows configured SSID, connection status, raw Wi-Fi status code, IP, channel, and signal when connected. -- `get wifi.ssid`: shows the configured Wi-Fi SSID. -- `get wifi.powersaving`: shows the current Wi-Fi power saving mode. -- `set wifi.ssid `: saves a Wi-Fi SSID and immediately retries connection. -- `set wifi.pwd `: saves a Wi-Fi password and immediately retries connection. -- `set wifi.powersaving none|min|max`: changes the Wi-Fi power save mode. - -Companion Wi-Fi builds also still support the existing rescue commands such as: - -- `set pin <6-digit-pin>` -- `rebuild` -- `erase` -- `ls ...` -- `cat ...` -- `rm ...` -- `reboot` - -### Recovery Access Point - -When a companion Wi-Fi device has no Wi-Fi credentials configured, or cannot connect -to its configured network for 60 seconds, it broadcasts its own `EastMesh-WiFi` -access point so it can be rescued without a serial cable. - -**The AP password is the device pin, zero-padded to 8 digits** (WPA2 requires at -least 8 characters). The pin follows the same rules as the Bluetooth pairing pin on -BLE builds: - -| Device | Active pin | `EastMesh-WiFi` password | -| --------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------- | -| Has a screen, no pin set | random 6-digit pin each boot, shown as `Pin:NNNNNN` on the home screen | `00NNNNNN` — read it off the screen | -| No screen, no pin set | `123456` (default) | `00123456` | -| Pin set via `set pin ` | your configured pin (any device) | your pin zero-padded to 8 digits, e.g. pin `4242` → `00004242` | - -Notes on the pin: - -- on devices with a screen, the pin is only random while no pin has been saved; run - `set pin ` for a fixed password (takes effect next boot) -- headless devices in public areas should always get a custom pin — `00123456` is a - documented default, so treat it like a default router password - -Recovery steps: - -1. join the `EastMesh-WiFi` network with the password from the table above -2. open the rescue CLI with `telnet 192.168.4.1` (or `nc 192.168.4.1 23`) — all - rescue commands above are available -3. `set wifi.ssid `, then `set wifi.pwd ` — the device immediately - retries the network with the new credentials -4. `reboot` (or just wait — see below) - -While the AP is up the device keeps retrying its configured network about once a -minute (expect a brief AP hiccup at each attempt); as soon as the station -connection succeeds, the recovery AP shuts down automatically (this also drops -your rescue session — that's the sign it worked). - -### Using the Companion App over the AP (Roaming) - -The recovery AP is not just for fixing credentials — the full companion protocol -is available through it, which makes it a roaming access mode when the device is -away from its configured network: - -1. join `EastMesh-WiFi` with the pin-derived password (see the table above) -2. in the MeshCore companion app, add/connect a Wi-Fi device with host - `192.168.4.1` and port `5000` -3. use the app normally — messages, contacts, channels all work over the AP - -Notes for roaming use: - -- run `set pin ` once so the AP password stays fixed across reboots; - otherwise devices with a screen pick a new random pin every boot -- the AP appears about 60 seconds after boot (the device tries its configured - network first), and stays up for as long as that network is unreachable -- back in range of its configured network, the device joins it and shuts the AP - down automatically — reconnect the app via the LAN address instead - -## Heap Health Check Script - -`eastmesh-tools/web-heap-check.sh [--stress] [--rounds N]` reads `memory`, `get web.status`, `get mqtt.status` and `get wifi.status` over the HTTPS API and, with `--stress`, simulates browser page-load bursts before re-reading `memory`. Authenticate with `REPEATER_PASSWORD` or `REPEATER_TOKEN` in the environment (or enter the password at the prompt). Watch `heap_max` (largest internal block; TLS needs ~40KB), `heap_min`, and the `heals`/`deferred`/`leaked` counters. diff --git a/eastmesh-docs/index.md b/eastmesh-docs/index.md deleted file mode 100644 index cbb1604b..00000000 --- a/eastmesh-docs/index.md +++ /dev/null @@ -1,72 +0,0 @@ -# MeshCore EastMesh Docs - -MeshCore-EastMesh keeps the upstream MeshCore firmware intact and publishes four firmware tracks, depending on how the device needs to connect: - -- `companion-wifi`: use this for Wi-Fi-connected companion devices. It stays closest to upstream MeshCore and adds the EastMesh Wi-Fi rescue/configuration commands. -- `repeater-bridge-espnow`: use this when you need a plain upstream-style repeater ESP-NOW bridge without MQTT uplink or the EastMesh web panel. -- `observer-eastmesh`: use this for a Wi-Fi repeater that should publish to an MQTT broker and, on supported ESP32 boards, offer the optional local web panel for setup and troubleshooting. -- `observer-eastmesh-bridge-espnow`: use this when one repeater needs both MQTT uplink and ESP-NOW bridge duties, including bridge channel/secret controls for keeping the bridge aligned with Wi-Fi. - -!!! note "Bridge tracks are local radio bridges" - - The ESP-NOW bridge tracks are for bridging two nearby repeaters that operate on different MeshCore radio configs, for example one repeater on `Australia (Narrow)` and another on `Australia (Mid)`. - - Bridge tracks are not MQTT-over-WAN, VPN, or internet tunnel releases. - -If you want guidance first, start with: - -- [Compare Boards](./boards.md) -- [Download and Flash Releases](./releases.md) - -If you already know your board and just want the quickest path, skip the docs and open the flasher: - -- [Open the EastMesh Flasher](https://flasher.eastmesh.au) - -## I Want To - -- choose a board: start with [Compare Boards](./boards.md) -- flash firmware with guidance: start with [Download and Flash Releases](./releases.md) -- flash firmware now: open the [EastMesh Flasher](https://flasher.eastmesh.au) -- set up a repeater after flashing: use [Download and Flash Releases](./releases.md) and [Use the Repeater Web Panel](./web-panel.md) -- understand EastMesh CLI commands: use [Custom CLI Commands](./custom-cli.md) -- automate or script against a repeater: use [Use the Repeater Web API](./api.md) -- build firmware locally: use [Build Locally With uv](./local-builds.md) - -## End User Guides - -- [Compare Boards](./boards.md) -- [Download and Flash Releases](./releases.md) -- [Migration From xJARiD/MeshCore](./migration.md) -- [Use the Repeater Web Panel](./web-panel.md) -- [Use the Repeater Web API](./api.md) -- [Custom CLI Commands](./custom-cli.md) - -## Developer Notes - -- [Build Locally With uv](./local-builds.md) - -## Current Scope - -This docs site only covers the EastMesh-specific pieces in this repository. - -For general MeshCore behaviour, radio operation, and upstream firmware concepts, refer to the upstream project: - -- [meshcore-dev/MeshCore](https://github.com/meshcore-dev/MeshCore) - -## Australian Regional References - -These references are community-maintained rather than official project documentation, but they are still useful for Australian regional setup notes, local conventions, and area-specific MeshCore references. - -### ACT / NSW / QLD / SA / TAS / VIC - -- [wiki.eastmesh.au](https://wiki.eastmesh.au/) -- [wiki.meshcoreaus.org](https://wiki.meshcoreaus.org/) - -### Sydney - -- [nswmesh.au](https://nswmesh.au/) -- [github.com/nswmesh](https://github.com/nswmesh/) - -### Brisbane - -- [wiki.mbug.com.au/en/Meshcore/Settings](https://wiki.mbug.com.au/en/Meshcore/Settings) diff --git a/eastmesh-docs/local-builds.md b/eastmesh-docs/local-builds.md deleted file mode 100644 index caf8e335..00000000 --- a/eastmesh-docs/local-builds.md +++ /dev/null @@ -1,200 +0,0 @@ -# Build Locally With uv - -This repo uses `uv` for Python tooling and runs PlatformIO through `uv run`. - -This page is for building from source. If you just want firmware to flash, start with [Download and Flash Releases](./releases.md) instead. - -Use this page when you are changing firmware, testing a target before release, or building a local artifact that is not available from GitHub Releases. - -## Setup - -From the repo root: - -```bash -uv sync -``` - -## Useful Commands - -List build targets: - -```bash -bash eastmesh-build.sh list -``` - -Plain PlatformIO build for a single target: - -```bash -uv run pio run -e heltec_v4_repeater_observer -uv run pio run -e heltec_v4_companion_radio_wifi -uv run pio run -e heltec_v4_repeater_bridge_espnow -uv run pio run -e heltec_v4_repeater_observer_espnow -uv run pio run -e Xiao_S3_WIO_repeater_observer_mqtt_bridge -``` - -Flash a target: - -```bash -uv run pio run -e heltec_v4_repeater_observer -t upload --upload-port /dev/tty.usbmodemXXXX -``` - -Serial monitor: - -```bash -uv run pio device monitor --port /dev/tty.usbmodemXXXX --baud 115200 -``` - -## Release-Style Local Builds - -If you want the same version metadata used by the release workflows, export the version variables first. - -Companion Wi-Fi: - -```bash -export FIRMWARE_VERSION=v1.14.1 -bash eastmesh-build.sh build-firmware heltec_v4_companion_radio_wifi -``` - -Observer: - -```bash -export FIRMWARE_VERSION=v1.15.0 -export EASTMESH_VERSION=v2026.5.1 -bash eastmesh-build.sh build-firmware heltec_v4_repeater_observer -``` - -Repeater ESP-NOW bridge: - -```bash -export FIRMWARE_VERSION=v1.15.0 -bash eastmesh-build.sh build-firmware heltec_v4_repeater_bridge_espnow -``` - -Observer ESP-NOW: - -```bash -export FIRMWARE_VERSION=v1.15.0 -export EASTMESH_VERSION=v2026.5.1 -bash eastmesh-build.sh build-firmware heltec_v4_repeater_observer_espnow -``` - -This produces versioned artifacts in `out/`. - -Versioning rule: - -- `companion-wifi` and `repeater-bridge-espnow` use the upstream MeshCore version as `FIRMWARE_VERSION` -- `observer-eastmesh`, `observer-eastmesh-bridge-espnow`, and `observer-eastmesh-bridge-mqtt` use the upstream MeshCore version as `FIRMWARE_VERSION` plus the EastMesh release version as `EASTMESH_VERSION` - -## Supported `repeater_observer` Boards - -These are the full PlatformIO env names used for local source builds and release artifact naming. - -```text -Generic_E22_sx1262_repeater_observer -Generic_E22_sx1268_repeater_observer -Heltec_E213_repeater_observer -Heltec_E290_repeater_observer -Heltec_T190_repeater_observer -heltec_tracker_v2_repeater_observer -Heltec_v2_repeater_observer -Heltec_v3_repeater_observer -heltec_v4_repeater_observer -heltec_v4_tft_repeater_observer -Heltec_Wireless_Paper_repeater_observer -Heltec_Wireless_Tracker_repeater_observer -Heltec_WSL3_repeater_observer -LilyGo_T3S3_sx1262_repeater_observer -LilyGo_T3S3_sx1276_repeater_observer -LilyGo_TBeam_1W_repeater_observer -LilyGo_TDeck_repeater_observer -LilyGo_Tlora_C6_repeater_observer -M5Stack_Unit_C6L_repeater_observer -Meshadventurer_sx1262_repeater_observer -Meshadventurer_sx1268_repeater_observer -Meshimi_repeater_observer -nibble_screen_connect_repeater_observer -RAK_3112_repeater_observer -Station_G2_logging_repeater_observer -Station_G2_repeater_observer -T_Beam_S3_Supreme_SX1262_repeater_observer -Tbeam_SX1262_repeater_observer -Tbeam_SX1276_repeater_observer -WHY2025_badge_repeater_observer -Xiao_C6_repeater_observer -Xiao_S3_LoRa2021F33-2G4_repeater_observer -Xiao_S3_WIO_repeater_observer -``` - -## Supported Bridge Boards - -Bridge targets can be listed from the repo root: - -```bash -bash eastmesh-build.sh list | grep '_repeater_bridge_espnow' -bash eastmesh-build.sh list | grep '_repeater_observer_espnow' -``` - -Common examples: - -```text -heltec_v4_repeater_bridge_espnow -heltec_v4_repeater_observer_espnow -Station_G2_repeater_bridge_espnow -Station_G2_repeater_observer_espnow -T_Beam_S3_Supreme_SX1262_repeater_bridge_espnow -T_Beam_S3_Supreme_SX1262_repeater_observer_espnow -``` - -## Supported `repeater_observer_mqtt_bridge` Boards - -An `mqtt_bridge` build variant is defined for each observer board. - -List the available targets: - -```bash -bash eastmesh-build.sh list | grep '_repeater_observer_mqtt_bridge' -``` - -Bridge firmware is for local ESP-NOW bridge use between nearby repeaters. It is not MQTT-over-WAN or VPN bridging. - -## Supported `companion_radio_wifi` Boards - -These are the full PlatformIO env names used for local source builds and release artifact naming. - -```text -heltec_tracker_v2_companion_radio_wifi -Heltec_v2_companion_radio_wifi -Heltec_v3_companion_radio_wifi -heltec_v4_companion_radio_wifi -heltec_v4_tft_companion_radio_wifi -Heltec_WSL3_companion_radio_wifi -LilyGo_TBeam_1W_companion_radio_wifi -LilyGo_TLora_V2_1_1_6_companion_radio_wifi -nibble_screen_connect_companion_radio_wifi -RAK_3112_companion_radio_wifi -Station_G2_companion_radio_wifi -T_Beam_S3_Supreme_SX1262_companion_radio_wifi -ThinkNode_M2_companion_radio_wifi -ThinkNode_M5_companion_radio_wifi -Xiao_S3_LoRa2021F33-2G4_companion_radio_wifi -Xiao_S3_WIO_companion_radio_wifi -``` - -## Companion Wi-Fi CLI - -Current companion Wi-Fi builds support persisted Wi-Fi rescue commands: - -- open a serial monitor at `115200` baud -- reboot the device -- long-press the user button within the first 8 seconds after boot to enter `CLI Rescue` -- wait for `========= CLI Rescue =========` -- then run the Wi-Fi rescue commands below from the serial monitor - -```text -get wifi.status -get wifi.ssid -get wifi.powersaving -set wifi.ssid -set wifi.pwd -set wifi.powersaving none|min|max -``` diff --git a/eastmesh-docs/migration.md b/eastmesh-docs/migration.md deleted file mode 100644 index 093df462..00000000 --- a/eastmesh-docs/migration.md +++ /dev/null @@ -1,121 +0,0 @@ -# Migration From `xJARiD/MeshCore` - -This page covers migrating from the previous firmware published at: - -- `https://github.com/xJARiD/MeshCore` - -to the EastMesh firmware published at: - -- `https://github.com/xJARiD/MeshCore-EastMesh` - -These steps are intended for existing repeater users moving to the new EastMesh `*_repeater_observer` firmware. - -Start by downloading the correct EastMesh release for your board from: - -- [Download and Flash Releases](./releases.md) - -## Quick Answer - -Try a normal firmware flash first, without erasing the device. After flashing, re-enter the current EastMesh Wi-Fi and MQTT settings listed below. - -Only do a full erase-flash if the repeater still behaves strangely after reflashing, reapplying settings, and rebooting. - -## Before You Start - -- you can flash the new firmware without erasing first -- after flashing, you will need to reapply your Wi-Fi and some MQTT settings -- you can do this either from the serial CLI or from the companion app - -Older MQTT bridge settings from `xJARiD/MeshCore` do not migrate into the current EastMesh MQTT uplink settings. Re-enter the settings you still use now instead of expecting older broker/analyzer values to carry forward. - -## Required Setup After Flashing - -Once the new firmware is flashed, set: - -- `set wifi.ssid ` -- `set wifi.pwd ` -- `set mqtt.iata ` -- `set mqtt.status on` - -## Optional Settings - -If you were previously using owner metadata, also set: - -- `set mqtt.owner <64-hex-char-public-key>` -- `set mqtt.email ` - -If you had previously disabled TX publishing, also set: - -- `set mqtt.tx off` - -## Final Step - -Once the settings are applied: - -- `reboot` - -After reboot, the repeater should be operational. - -## Verify Operation - -Confirm the repeater is up and connected with: - -- `get wifi.status` -- `get mqtt.status` - -## Validate In The Web Console - -If the web panel is enabled on your board, you can also verify through the local HTTPS admin page. - -Enable and check it with: - -- `set web on` -- `get web.status` - -Then open: - -- `https:///` - -Notes: - -- ignore the browser warning for the self-signed certificate -- log in with the admin password -- this is the same admin password used for admin access via the companion app - -## Legacy MQTT Commands - -Some MQTT CLI commands from the previous firmware do not apply in EastMesh and should not be migrated. - -EastMesh stores its current Wi-Fi and MQTT uplink settings separately from the older firmware, so older broker and analyzer settings do not carry forward in a useful way. After flashing, re-enter the settings you still use now, rather than expecting the old MQTT bridge configuration to map across. - -These previous MQTT CLI commands no longer apply in EastMesh: - -- `get mqtt.server` -- `get mqtt.port` -- `get mqtt.password` -- `get mqtt.analyzer.us` -- `get mqtt.analyzer.eu` -- `get mqtt.config.valid` - -If you flash without erase first, some old values may still remain in flash in the background. The important point is that EastMesh does not use those old MQTT bridge values for its current uplink configuration, so users should not expect them to carry forward or have any effect. - -## Recommended Migration Flow - -1. Flash the new EastMesh firmware without erasing. -2. Open the serial CLI or companion app. -3. Set `wifi.ssid`, `wifi.pwd`, `mqtt.iata`, and `mqtt.status on`. -4. Optionally set `mqtt.owner` and `mqtt.email`. -5. If you previously had TX publishing disabled, set `mqtt.tx off`. -6. Reboot the repeater. -7. Verify with `get wifi.status` and `get mqtt.status`. -8. Optionally confirm local web access with `set web on`, `get web.status`, and `https:///`. - -## Related Docs - -- [Repeater Web Panel](./web-panel.md) -- [Custom CLI Commands](./custom-cli.md) -- [Download and Flash Releases](./releases.md) - -## Last Resort - -If the repeater still does not come up cleanly after reflashing, reapplying settings, and rebooting, the last resort is to perform a full erase-flash and then configure it again from scratch. diff --git a/eastmesh-docs/releases.md b/eastmesh-docs/releases.md deleted file mode 100644 index d4218bc8..00000000 --- a/eastmesh-docs/releases.md +++ /dev/null @@ -1,209 +0,0 @@ -# Download And Flash Releases - -EastMesh release assets are published on: - -- - -## Start Here - -If this is your first time flashing EastMesh firmware, the easiest path is: - -1. Open . -2. Select the firmware that matches what the device will do. -3. Select the board that matches your exact hardware. -4. Select the version. The flasher defaults to the latest available release. -5. Select the image type: - - `Update` for a normal firmware update - - `Full Flash` for a clean full-image flash -6. Click `Flash Firmware`. -7. After flashing, finish setup with `Open MeshCore Config Panel` or the `Serial Console`. - -If you are not sure which track you need, start with `companion-wifi` for app-connected companion devices or `observer-eastmesh` for a fixed repeater that should publish to MQTT. - -## Pick Your Track - -EastMesh publishes four release tracks: - -| Track | Use it when | Firmware filename suffix | -| --------------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------- | -| `companion-wifi` | You want a companion device that connects over Wi-Fi instead of BLE or USB. | `*_companion_radio_wifi` | -| `observer-eastmesh` | You want a repeater with Wi-Fi and MQTT uplink, usually feeding broker visibility such as EastMesh/CoreScope. | `*_repeater_observer` | -| `repeater-bridge-espnow` | You want a local ESP-NOW bridge between nearby repeaters, without MQTT uplink or the EastMesh web panel. | `*_repeater_bridge_espnow` | -| `observer-eastmesh-bridge-espnow` | You want one repeater to provide both MQTT uplink and local ESP-NOW bridge duties. | `*_repeater_observer_espnow` | - -!!! note "Bridge firmware is not a WAN bridge" - - ESP-NOW bridge tracks are for bridging two nearby repeaters that operate on different MeshCore radio configs, for example `Australia (Narrow)` and `Australia (Mid)`. - - Bridge tracks do not use MQTT uplink brokers to tunnel mesh traffic over the internet, WAN links, or VPNs. - -## Pick The Right Asset - -Download the asset that matches your board and firmware type. - -Examples: - -- `heltec_v4_companion_radio_wifi-v1.14.1-abcdef.bin` -- `heltec_v4_repeater_observer-v1.15.0-eastmesh-v2026.5.1-abcdef.bin` -- `heltec_v4_repeater_observer-v1.15.0-eastmesh-v2026.5.1-abcdef-merged.bin` -- `heltec_v4_repeater_bridge_espnow-v1.15.0-abcdef.bin` -- `heltec_v4_repeater_observer_espnow-v1.15.0-eastmesh-v2026.5.1-abcdef.bin` - -The important part is the board/env prefix: - -- `*_companion_radio_wifi` -- `*_repeater_observer` -- `*_repeater_bridge_espnow` -- `*_repeater_observer_espnow` - -## Which File To Flash - -If you are using , choose the image type in the flasher: - -- `Update` = normal firmware update -- `Full Flash` = clean full-image flash - -If you are manually downloading files from GitHub Releases, use the filename instead: - -Use the standard `.bin` file when you are updating an existing device with the same target and partition layout. - -Use the `-merged.bin` file when you want a clean install after erasing flash. This is the full ESP32 image and is intended to be flashed from address `0x0`. - -Practical rule: - -- `.bin` = incremental update -- `-merged.bin` = erase and clean flash - -## Flashing Flow - -1. Select firmware. -2. Select board. -3. Select version. The latest version is selected by default. -4. Select image type: `Update` or `Full Flash`. -5. Click `Flash Firmware`. -6. After flashing, use `Open MeshCore Config Panel` or the `Serial Console` for post-flash setup such as Wi-Fi, MQTT, bridge, or radio settings. - -## Recommended Flasher - -The recommended flasher is: - -- - -It includes native support for the common EastMesh firmware types and can also flash custom firmware files: - -- `companion_radio_wifi` firmware -- `repeater_observer` firmware -- custom firmware files - -For bridge releases, use the matching bridge option if the flasher shows one. Otherwise, download the release asset yourself and flash it as a custom firmware file. - -Recommended usage: - -- use `Update` when you are updating an existing device -- use `Full Flash` when you want a clean full-image flash - -After flashing, there are two useful setup paths in the flasher. - -### Open MeshCore Config Panel - -Use `Open MeshCore Config Panel` to access the guided MeshCore setup tools: - -- `Repeater Setup` -- `Console` - -#### Repeater Setup - -`Repeater Setup` is the guided first-time repeater flow. - -It is the traditional way to configure a repeater after flashing, including: - -- device name -- latitude and longitude -- admin and guest passwords -- radio settings, including preset selection -- advert interval -- flood advert interval -- flood max -- some advanced repeater settings - -As of `v1.2.1`, the local repeater web panel also includes the same common repeater settings, so users can complete initial setup there and return for occasional troubleshooting or configuration changes. On observers that need maximum headroom, it is still best to disable the panel again when you are finished. - -#### Console - -`Console` is the raw CLI interface. - -It is especially useful, and often required, for initial Wi-Fi setup on Wi-Fi-capable firmware tracks: - -- `set wifi.ssid ` -- `set wifi.pwd ` - -This applies to: - -- `companion_radio_wifi` -- `repeater_observer` -- `repeater_observer_espnow` - -### Serial Console - -Use `Serial Console` when you want to connect over USB and type CLI commands directly. - -The flasher can connect and disconnect from the device over USB. It also includes preset CLI commands that populate the command input for easier setup: - -- `set wifi.ssid` -- `set wifi.pwd` -- `get wifi.status` -- `get mqtt.status` -- `set web on` -- `get web.status` - -For `set` commands, complete the command in the input box before sending it. For example, select `set wifi.ssid`, add your Wi-Fi network name, then press `Send`. - -## Common First Steps - -### Observer - -`repeater_observer` builds include the EastMesh MQTT additions. Depending on the board, they may also include the local web panel. - -Typical first steps after flashing: - -- set `wifi.ssid` -- set `wifi.pwd` -- set `mqtt.iata` -- confirm `get mqtt.status` -- optionally set `mqtt.owner` and `mqtt.email` -- optionally enable a second broker such as `meshmapper` - -### Observer ESP-NOW - -`repeater_observer_espnow` builds combine the observer role with local ESP-NOW bridge support. - -Typical first steps after flashing: - -- set `wifi.ssid` -- set `wifi.pwd` -- set `mqtt.iata` -- confirm `get mqtt.status` -- check `get wifi.status` and note the connected Wi-Fi channel -- set `bridge.channel` to match that Wi-Fi channel -- set the same `bridge.secret` on every local ESP-NOW bridge node that should talk together - -### Repeater ESP-NOW Bridge - -`repeater_bridge_espnow` builds are for local ESP-NOW bridge nodes without MQTT uplink. - -Typical first steps after flashing: - -- configure the normal repeater settings for the board -- set the intended MeshCore radio preset/config -- set the same bridge channel and secret on the local bridge pair -- confirm both bridge repeaters are physically nearby enough for ESP-NOW to work - -### Companion Wi-Fi - -`companion_radio_wifi` builds are for companion devices that expose the app interface over Wi-Fi instead of BLE or USB. - -Typical first steps after flashing: - -- set Wi-Fi credentials -- confirm `get wifi.status` -- connect your client app to the companion over Wi-Fi diff --git a/eastmesh-docs/web-panel.md b/eastmesh-docs/web-panel.md deleted file mode 100644 index dce014b8..00000000 --- a/eastmesh-docs/web-panel.md +++ /dev/null @@ -1,433 +0,0 @@ -# Repeater Web Panel - -This page is for end users running an EastMesh `*_repeater_observer` build with the local web panel enabled. - -It covers how to reach the panel, what each section does, and what to expect when using it on desktop or mobile. - -## Start Here - -For normal first-time use: - -1. Connect the repeater to Wi-Fi. -2. Run `get wifi.status` to find its IP address. -3. Open `https:///` in a browser. -4. Accept the self-signed certificate warning. -5. Log in with the repeater admin password. -6. Use the panel for setup or troubleshooting. -7. When finished, use `set web off` if the repeater needs maximum MQTT headroom. - -Most users only need the panel for first setup, occasional setting changes, and troubleshooting. Leave it enabled only when browser access is worth the extra memory use. - -## What It Is - -The repeater web panel is a local HTTPS configuration page served directly by the repeater over Wi-Fi. - -It gives you: - -- a password-gated local admin page at `/app` -- a dedicated stats and trends page at `/stats` -- quick `get` commands for common repeater and MQTT checks -- a terminal-style CLI panel for full repeater CLI access -- editable repeater settings -- editable MQTT settings -- a historical stats view with trends, neighbours, and recent events - -Operational guidance: - -- use it for initial setup, occasional configuration changes, and troubleshooting -- when you are finished, prefer `set web off` on observers that need maximum headroom -- this leaves more internal heap available for MQTT/WSS activity, especially on dual-broker setups - -## Common Tasks - -### Check Wi-Fi And MQTT - -1. Open the panel. -2. Press `wifi.status` in Quick `get` Commands. -3. Press `mqtt.status` in Quick `get` Commands. -4. Open `/stats` from the top navigation for the historical stats view. - -### Change Device Name - -1. Edit `Device Name`. -2. Press `Save`. -3. Confirm the generated command and reply in the CLI terminal box. - -### Update MQTT Owner Or Email - -1. Go to `MQTT Settings`. -2. Enter the new value. -3. Press `Save`. -4. Use the refresh button if you want to re-read the stored value from the repeater. - -## Screenshot Overview - -The screenshots below show the current split between the lighter `/app` admin page and the dedicated `/stats` status page. - -### `/app` screenshot - -![Repeater web panel `/app` overview](./_assets/repeater_web_panel_app_light.png#only-light) -![Repeater web panel `/app` overview](./_assets/repeater_web_panel_app_dark.png#only-dark) - -### `/stats` screenshot - -![Repeater web panel `/stats` overview](./_assets/repeater_web_panel_stats_light.png#only-light) -![Repeater web panel `/stats` overview](./_assets/repeater_web_panel_stats_dark.png#only-dark) - -## Requirements - -You need: - -- a supported `*_repeater_observer` firmware build -- Wi-Fi configured on the repeater -- the repeater connected to your local network -- the repeater admin password - -Some constrained targets disable the web panel to stay within flash limits. If your board does not support it, `get web.status` will not show it as available. - -## How To Open It - -1. Connect the repeater to Wi-Fi. -2. Find its IP address. -3. Open `https:///` in a browser. -4. Accept the browser warning for the self-signed certificate. -5. Enter the repeater admin password. - -Useful CLI commands: - -- `get wifi.status`: shows Wi-Fi state, IP address, channel, and signal when connected. -- `get web.status`: shows whether the web panel is up and which URL to use. - -Example: - -- `https://10.33.135.208/` - -## Login And Security - -- the panel uses the same admin password as the repeater CLI -- the connection is HTTPS, but the certificate is self-signed -- browsers will warn the first time you connect -- the panel exposes the repeater CLI after login -- the browser keeps the session token while changing between `/app` and `/stats`; the page switches views in place until logout, idle lock, or device restart -- brief Wi-Fi drops can interrupt in-flight page loads, but should not force a new login when the web panel restarts -- after a device restart or reflash, the app checks the stored token before loading settings and returns to login if the session is stale - -This is intended for local admin use on a trusted network, not for open internet exposure. - -## Performance Notes - -The panel is designed to load more gently than earlier versions. On login it now fetches sections in sequence instead of requesting one large bootstrap payload up front. - -Even with that change, the panel still uses HTTPS and internal heap. On boards running one or two WSS MQTT brokers, opening the panel reduces MQTT headroom while the session is active. - -Recommended practice for repeater deployments: - -- enable the panel for initial configuration -- use it again for occasional checks or troubleshooting -- disable it with `set web off` when finished so MQTT has the most headroom available - -## Navigation And Actions - -The web console now has two main pages: - -- `/app`: lighter-weight control and configuration view -- `/stats`: current status, trends, neighbours, and recent events - -Both pages share the same top navigation and utility actions. - -### `/app` - -The `/app` page is the main admin and configuration surface. - -It includes: - -- navigation to `App` and `Stats` -- `Advert` -- `Start OTA` -- `Reboot` -- theme toggle -- `Logout` - -Use `Start OTA` only when you intend to update firmware. - -If the browser can reach the EastMesh flasher firmware manifest, the app page checks the EastMesh release version from `mqtt.client_version` against published MeshCore-EastMesh releases. When a newer EastMesh release tag is available, a firmware update notice appears at the top of the page. - -For builds that include `CLIENT_ENV`, the notice can show `Update now`. This downloads the matching non-merged `.bin` from the EastMesh flasher firmware mirror, shows progress in the banner, uploads the firmware through the HTTPS web panel, and lets the device reboot. This requires CORS headers on the flasher `/firmwares/` paths. - -### `/stats` - -The `/stats` page is the home for current status and historical visibility. - -It includes: - -- navigation to `App` and `Stats` -- `Refresh` -- `Reboot` -- theme toggle -- `Logout` - -## Quick "get" Commands - -This section runs common read-only commands for: - -- Wi-Fi -- MQTT - -These are useful for quick checks without typing into the CLI field. The MQTT quick actions include `mqtt.status`, `mqtt.client_version`, `mqtt.iata`, `mqtt.owner`, and `mqtt.email`. - -## Run CLI Command - -This is a small terminal for the repeater CLI. - -- press `Enter` to run the command -- command history is shown in the terminal box below -- save buttons elsewhere in the page also show the generated command and the reply here -- `clock` is available here if you want to check the repeater's current board time -- authenticated sessions can run the same CLI commands accepted by the repeater - -This makes it easy to see exactly what the panel sent to the repeater. - -## Repeater Settings - -This section includes: - -- Device Name -- Clock UTC -- Latitude -- Longitude -- Guest Password -- Private Key -- Advert Interval -- Flood Interval -- Scoped Flood Max -- Unscoped Flood Max -- Owner Info - -Notes: - -- `Latitude` and `Longitude` default to `0.0` as placeholders -- changing the private key requires a reboot to apply -- the refresh buttons load the current value from the repeater -- the save buttons send the matching CLI command immediately - -## Radio Settings And Regions - -The `Radio Settings` section includes the community radio preset selector, path hash mode, and a `Region (Australia)` dropdown derived from the state groups used by the MQTT IATA selector. The dropdown is preselected only when the repeater already has a matching allowed `au-STATE` region. Pressing the region `Save` button creates and allows the base `au` region plus the selected state region, then persists the region map with `region save`. - -## Info - -This section shows: - -- `Version`: firmware version with build date -- `Client Version`: MQTT client version string -- `Public Key` - -## Ghost Node Mode - -Ghost Node Mode is a convenience control on `/app` for a repeater that should stay on Wi-Fi and MQTT, but should not actively behave like another nearby repeater. - -Typical use case: - -- an indoor or colocated MQTT observer where another repeater nearby is already doing the RF relay work -- a node you want feeding MQTT, web status, and troubleshooting data without also adding extra repeat traffic or adverts - -When enabled, Ghost Node Mode: - -- turns `repeat` off -- sets `advert.interval` to `0` -- sets `flood.advert.interval` to `0` -- leaves the local web panel and MQTT features running - -When disabled, the panel restores the prior repeat and advert settings if it still knows them from the current browser session. If not, it falls back to: - -- `repeat on` -- `advert.interval 60` -- `flood.advert.interval 12` - -This mode is useful when you want the device to observe and publish, not to act as an additional RF repeater. It does not create a separate firmware role; it is just a grouped web-panel shortcut for those existing settings. - -## MQTT Settings - -This section includes: - -- `mqtt.iata`: selected from a curated east-coast/south-east list. -- `mqtt.owner`: owner public key. -- `mqtt.email`: owner contact email. -- MQTT brokers: **Primary MQTT** and **Secondary MQTT** dropdowns, each selecting one of `eastmesh-au`, `meshmapper`, `Custom`, the retired `letsmesh-eu`/`letsmesh-us`, or `None`. The two slots enforce the two-broker maximum, and a broker chosen in one slot is disabled in the other. -- custom MQTT `host:port`, TCP/WSS transport, username, and password fields, shown when `Custom` is selected in either slot. - -`UNSET - To be configured` is the default for new observer installs until a real saved value exists. - -Notes: - -- when `mqtt.iata` is `UNSET`, the panel shows a banner at the top reminding you to set it under MQTT Settings -- while `mqtt.iata` is `UNSET`, enabled MQTT brokers do not attempt to connect -- the current MQTT server states are loaded when the page opens -- you can toggle each MQTT server on or off from this panel -- custom MQTT uses the configured username and password, not JWT authentication -- custom MQTT defaults to TCP; choose WSS for MQTT over secure WebSockets using the fixed `/mqtt` websocket path and the ESP-IDF x509 root CA bundle -- turning off a connected MQTT server publishes retained offline status before the client disconnects -- changing `mqtt.iata` away from a configured value publishes retained offline status to the old status topic, restarts connected broker clients, and reconnects under the new topic path -- at most two MQTT brokers can be enabled at once - -## `/stats` Overview - -The stats page is loaded separately from `/app` and is intended to keep the main admin page lighter. - -The `/stats` page currently shows: - -- `Services`: MQTT, web, archive, neighbour count, and, when mounted, card and archive capacity -- optional full-width `Environment` summary card on boards that report GPS or environmental telemetry -- `Trends`: battery, heap free, packet activity, signal, noise floor, and, when GPS is active, satellites -- `Neighbours`: current neighbour table with ID, SNR, heard age, and advert age -- `Events`: current boot/session events - -Planned MQTT JWT rotations are shown as `mqtt_token_refreshed` events instead of a short `mqtt_disconnected` / `mqtt_connected` pair. - -For boards that expose extra telemetry, the optional `Environment` summary card can show current values such as GPS active/fix state, latitude, longitude, GPS altitude, voltage, sensor temperature, humidity, barometer, pressure-derived altitude, and MCU temperature. - -Metrics with no current value are hidden rather than showing placeholder rows, so the cards vary by board and by current sensor state. - -The `Core` battery meter prefers a board-reported battery percentage when the target exposes one. On those boards, the meter detail shows the live battery millivolt reading only. Otherwise it scales the displayed percentage from the board's configured battery voltage range and shows that range in the detail text rather than assuming a fixed single-cell `3000-4200 mV` pack. - -The trend graphs load sequentially rather than as one large payload: - -1. summary/status -2. battery -3. memory -4. packet activity -5. signal -6. satellites when GPS is active - -This keeps browser-side and device-side memory use lower than the previous in-page stats view. - -If `web.stats` is enabled and an SD archive is mounted, trends can restore archived summary points after reboot from the latest SD snapshot. Recent live points are still added from in-memory history. - -### Stats History Capacity - -Stats samples are collected once per minute. - -Current in-memory history caps are: - -| Board class | Sample cap | Event cap | Approx. sampled history | -| -------------------------------- | ---------: | --------: | ------------------------ | -| No PSRAM | `24` | `8` | Live-only recent history | -| Less than `4 MB` PSRAM | `240` | `96` | About `4` hours | -| `4 MB` to less than `8 MB` PSRAM | `480` | `192` | About `8` hours | -| `8 MB` PSRAM or more | `720` | `288` | About `12` hours | - -When `web.stats` is enabled, stats history starts capturing from boot or from the time it is enabled, even if `/stats` has not been opened yet. No-PSRAM boards use the smaller live-only buffer shown above. - -Archive-backed restore requires `web.stats` enabled plus a mounted SD card on boards that support the EastMesh archive path. - -If archive access drops at runtime, the repeater retries the SD mount periodically while `web.stats` remains enabled. - -The main purpose of the SD card is to let the repeater retain and restore stats history for `/stats`. The archive keeps fast `.latest` snapshot files for quick restore and UTC-dated daily `.log` files for longer-term history. As a secondary option, those files can also be removed and inspected on a computer for deeper manual review. - -On no-PSRAM boards, `/stats` can still show recent graphs, but the history is smaller and does not provide the same archive-backed behaviour as PSRAM-capable boards. - -Useful CLI commands: - -- `set web.stats on` -- `set web.stats off` -- `get web.stats.status` - -## Mobile Use - -The page is responsive and should work cleanly on a phone. - -On mobile: - -- quick command buttons collapse into a two-column layout -- top navigation and action groups stay compact and touch-friendly -- input rows stay usable for touch interaction -- trend cards reorganise into single-column sections where needed - -## Advanced Tasks - -### Start OTA - -1. Press `Start OTA`. -2. Confirm the action. -3. The panel starts OTA, waits for the OTA HTTP listener to take over port `80`, then opens the returned `http://.../update` URL. -4. The local HTTP redirect listener on port `80` is released so OTA can take over that port. -5. Continue with your normal OTA workflow. - -If the repeater is already connected to Wi-Fi, OTA uses the existing LAN address. If Wi-Fi is not connected, the repeater starts the `MeshCore-OTA` access point and returns that OTA address instead. -If the old redirect listener has not fully released port `80`, the OTA listener retries for up to about 30 seconds before giving up. - -If an older build sends you through a strange redirect after `start ota`, use the web `Start OTA` button to begin the upgrade. This redirect issue is fixed in `observer-eastmesh-v1.3.11`. - -### Purge SD - -`Purge SD` appears as red inline text next to `Archive Used` in the `/stats` Services card. It asks for confirmation, then runs the authenticated `purge sd` command. - -This deletes files and directories from the mounted SD card archive, then recreates the empty `/stats` archive directory for continued runtime capture. It does not erase the internal repeater filesystem, identity, prefs, regions, radio settings, or MQTT settings. - -### Use Historical Stats - -1. Enable stats if needed with `set web.stats on`. -2. Open `/stats` from the top navigation. -3. Review `Services` for archive and runtime state. -4. Review `Trends` for recent graph history. -5. Use `Refresh` to reload the stats page. - -## Troubleshooting - -### The browser warns about the certificate - -That is expected. The panel uses a self-signed certificate generated for local use. - -### I cannot reach the page - -Check: - -- the repeater is on Wi-Fi -- the IP address from `get wifi.status` -- `get web.status` reports the panel as up -- your board/firmware target supports the web panel - -### The panel opens but login fails - -Use the repeater admin password, not the guest password. - -### MQTT becomes unstable when I log in - -The web panel now loads settings section-by-section to reduce startup pressure, but HTTPS still consumes internal heap. - -Check: - -- whether one or two MQTT brokers are enabled -- `memory` before and after login -- whether stability improves after `set web off` - -For fixed installations where MQTT uptime matters more than browser access, use the panel briefly and then disable it again. - -### HTTP opens instead of HTTPS - -The repeater now redirects plain `http://` requests to the local `https://` panel URL. If the browser still shows a connection problem after redirecting, open `https:///` directly and accept the self-signed certificate warning first. - -### Stats or settings do not refresh - -Try: - -- refreshing the browser tab -- using `Refresh` on `/stats` -- logging out and back in -- checking Wi-Fi stability with `get wifi.status` - -### `/stats` is unavailable - -Check: - -- `get web.status` -- `get web.stats.status` -- whether `set web.stats on` has been applied - -If `web.stats` is off, `/stats` will stay disabled and the historical graph requests will not run. - -## Related Docs - -- [Custom CLI Commands](./custom-cli.md) -- [Download and Flash Releases](./releases.md) -- [Build Locally With uv](./local-builds.md) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 051f3b31..acf1cfd1 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -72,8 +72,14 @@ public: display.setColor(UIColor::secondary_txt); display.setTextSize(1); + + #ifdef OLED_RU + char filtered_date[sizeof(FIRMWARE_BUILD_DATE)]; + display.translateUTF8ToBlocks(filtered_date, FIRMWARE_BUILD_DATE, sizeof(filtered_date)); + display.drawTextCentered(display.width()/2, 42, filtered_date); + #else display.drawTextCentered(display.width()/2, 48, FIRMWARE_BUILD_DATE); - + #endif return 1000; } diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index cdc0f7a9..80694386 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -374,6 +374,9 @@ void escapeJsonString(const char* input, char* output, size_t output_size) { #define FIRMWARE_VER_LEVEL 2 +#define PATH_BLACKLIST_FILE "/path_bl" +#define CHAN_BLACKLIST_FILE "/chan_bl" + #define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS #define REQ_TYPE_KEEP_ALIVE 0x02 #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 @@ -894,6 +897,301 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { return getRNG()->nextInt(0, 5*t + 1); } +/* ----------------------- Blacklist helpers -------------------------------- */ + +// Returns true if any path entry in the packet matches a path-prefix blacklist entry. +// Comparison uses min(entry.len, hash_size) bytes, so shorter entries match any hash size. +bool MyMesh::isPathBlacklisted(const mesh::Packet* packet) const { + uint8_t hash_size = packet->getPathHashSize(); + uint8_t hash_count = packet->getPathHashCount(); + const uint8_t* ptr = packet->path; + for (uint8_t h = 0; h < hash_count; h++, ptr += hash_size) { + for (int b = 0; b < MAX_BLACKLIST_ENTRIES; b++) { + if (_path_blacklist[b].len == 0) continue; + uint8_t cmp_len = (_path_blacklist[b].len < hash_size) ? _path_blacklist[b].len : hash_size; + if (memcmp(ptr, _path_blacklist[b].prefix, cmp_len) == 0) return true; + } + } + return false; +} + +// Returns true if the channel hash at the start of a GRP_TXT/GRP_DATA payload matches +// a channel-hash blacklist entry (hex prefix or #channel_name with decrypt verification). +bool MyMesh::isChanBlacklisted(const mesh::Packet* packet) const { + uint8_t pt = packet->getPayloadType(); + if (pt != PAYLOAD_TYPE_GRP_TXT && pt != PAYLOAD_TYPE_GRP_DATA) return false; + if (packet->payload_len < PATH_HASH_SIZE) return false; + + // Check hex prefix entries against the channel-hash region only. + // Bytes after PATH_HASH_SIZE are MAC/ciphertext, not part of the stable channel identifier. + for (int b = 0; b < MAX_BLACKLIST_ENTRIES; b++) { + if (_chan_blacklist[b].len == 0) continue; + uint8_t avail = PATH_HASH_SIZE; + uint8_t cmp_len = (_chan_blacklist[b].len < avail) ? _chan_blacklist[b].len : avail; + if (memcmp(packet->payload, _chan_blacklist[b].prefix, cmp_len) == 0) return true; + } + + // Check #channel_name entries with test decryption + if (_num_chan_name_filters > 0 && packet->payload_len > PATH_HASH_SIZE + CIPHER_MAC_SIZE) { + uint8_t channel_hash = packet->payload[0]; + const uint8_t* macAndData = &packet->payload[PATH_HASH_SIZE]; // MAC + encrypted data + int enc_len = packet->payload_len - PATH_HASH_SIZE; + + for (int i = 0; i < _num_chan_name_filters; i++) { + // Quick hash check first + if (channel_hash != _chan_name_filters[i].hash[0]) continue; + // Try to decrypt to verify this is actually the channel + uint8_t tmp[MAX_PACKET_PAYLOAD]; + int len = mesh::Utils::MACThenDecrypt(_chan_name_filters[i].secret, tmp, macAndData, enc_len); + if (len > 0) return true; // Successfully decrypted, confirmed channel match + } + } + + return false; +} + +bool MyMesh::addToBlacklist(BlacklistEntry* list, const uint8_t* prefix, uint8_t len) { + if (len == 0 || len > MAX_PATH_PREFIX_LEN) return false; + // if already present, consider success + for (int i = 0; i < MAX_BLACKLIST_ENTRIES; i++) { + if (list[i].len == len && memcmp(list[i].prefix, prefix, len) == 0) return true; + } + // find an empty slot + for (int i = 0; i < MAX_BLACKLIST_ENTRIES; i++) { + if (list[i].len == 0) { + list[i].len = len; + memcpy(list[i].prefix, prefix, len); + return true; + } + } + return false; // list is full +} + +bool MyMesh::removeFromBlacklist(BlacklistEntry* list, const uint8_t* prefix, uint8_t len) { + for (int i = 0; i < MAX_BLACKLIST_ENTRIES; i++) { + if (list[i].len == len && memcmp(list[i].prefix, prefix, len) == 0) { + list[i].len = 0; // mark slot as empty + return true; + } + } + return false; +} + +void MyMesh::formatBlacklist(const BlacklistEntry* list, char* reply) { + char* dp = reply; + int count = 0; + for (int i = 0; i < MAX_BLACKLIST_ENTRIES; i++) { + if (list[i].len == 0) continue; + if (count > 0) *dp++ = '\n'; + mesh::Utils::toHex(dp, list[i].prefix, list[i].len); + dp += list[i].len * 2; + count++; + } + if (count == 0) { + strcpy(reply, "-none-"); + } else { + *dp = 0; + } +} + +void MyMesh::loadBlacklist(const char* fname, BlacklistEntry* list) { + memset(list, 0, sizeof(BlacklistEntry) * MAX_BLACKLIST_ENTRIES); +#if defined(RP2040_PLATFORM) + File f = _fs->open(fname, "r"); +#else + File f = _fs->open(fname); +#endif + if (!f) return; + int idx = 0; + char line[MAX_PATH_PREFIX_LEN * 2 + 4]; + int line_len = 0; + while (f.available() && idx < MAX_BLACKLIST_ENTRIES) { + int c = f.read(); + if (c < 0) break; + if (c == '\n' || c == '\r') { + if (line_len >= 2 && (line_len % 2 == 0)) { + line[line_len] = 0; + uint8_t prefix[MAX_PATH_PREFIX_LEN]; + int byte_len = line_len / 2; + if (byte_len <= MAX_PATH_PREFIX_LEN && mesh::Utils::fromHex(prefix, byte_len, line)) { + list[idx].len = (uint8_t)byte_len; + memcpy(list[idx].prefix, prefix, byte_len); + idx++; + } + } + line_len = 0; + } else if (line_len < (int)(sizeof(line) - 1)) { + line[line_len++] = (char)c; + } + } + // handle last line with no trailing newline + if (line_len >= 2 && (line_len % 2 == 0) && idx < MAX_BLACKLIST_ENTRIES) { + line[line_len] = 0; + uint8_t prefix[MAX_PATH_PREFIX_LEN]; + int byte_len = line_len / 2; + if (byte_len <= MAX_PATH_PREFIX_LEN && mesh::Utils::fromHex(prefix, byte_len, line)) { + list[idx].len = (uint8_t)byte_len; + memcpy(list[idx].prefix, prefix, byte_len); + } + } + f.close(); +} + +void MyMesh::saveBlacklist(const char* fname, const BlacklistEntry* list) { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + _fs->remove(fname); + File f = _fs->open(fname, FILE_O_WRITE); +#elif defined(RP2040_PLATFORM) + File f = _fs->open(fname, "w"); +#else + File f = _fs->open(fname, "w", true); +#endif + if (!f) return; + for (int i = 0; i < MAX_BLACKLIST_ENTRIES; i++) { + if (list[i].len == 0) continue; + char hex[MAX_PATH_PREFIX_LEN * 2 + 2]; + mesh::Utils::toHex(hex, list[i].prefix, list[i].len); + f.println(hex); + } + f.close(); +} + +/* ------------------- Channel name filter helpers -------------------------- */ + +void MyMesh::deriveChanNameFilter(ChanNameFilter& entry, const char* name) { + // Derive channel secret: first 16 bytes of sha256(name), zero-padded to PUB_KEY_SIZE + // to match GroupChannel.secret layout (MACThenDecrypt reads PUB_KEY_SIZE bytes) + uint8_t full_hash[32]; + mesh::Utils::sha256(full_hash, 32, (const uint8_t*)name, strlen(name)); + memset(entry.secret, 0, sizeof(entry.secret)); + memcpy(entry.secret, full_hash, CIPHER_KEY_SIZE); + // Derive channel hash from the secret (using 16-byte key length, matching addChannel) + mesh::Utils::sha256(entry.hash, sizeof(entry.hash), entry.secret, CIPHER_KEY_SIZE); + StrHelper::strncpy(entry.name, name, sizeof(entry.name)); +} + +bool MyMesh::addChanNameFilter(const char* name) { + // Truncate name if too long + char safe_name[sizeof(ChanNameFilter::name)]; + strncpy(safe_name, name, sizeof(safe_name) - 1); + safe_name[sizeof(safe_name) - 1] = '\0'; + + // Check for duplicate using SAFE name + for (int i = 0; i < _num_chan_name_filters; i++) { + if (strcmp(_chan_name_filters[i].name, safe_name) == 0) return true; // already exists + } + if (_num_chan_name_filters >= MAX_CHAN_NAME_FILTERS) return false; // full + deriveChanNameFilter(_chan_name_filters[_num_chan_name_filters], safe_name); + _num_chan_name_filters++; + return true; +} + +bool MyMesh::removeChanNameFilter(const char* name) { + for (int i = 0; i < _num_chan_name_filters; i++) { + if (strcmp(_chan_name_filters[i].name, name) == 0) { + // shift remaining entries down + for (int j = i; j < _num_chan_name_filters - 1; j++) { + _chan_name_filters[j] = _chan_name_filters[j + 1]; + } + _num_chan_name_filters--; + memset(&_chan_name_filters[_num_chan_name_filters], 0, sizeof(ChanNameFilter)); + return true; + } + } + return false; +} + +void MyMesh::loadChanBlacklist(const char* fname) { + // Load both hex prefix entries and #channel_name entries from the same file + loadBlacklist(fname, _chan_blacklist); // loads hex entries into _chan_blacklist + + // Now re-read the file to pick up #channel_name lines + _num_chan_name_filters = 0; + memset(_chan_name_filters, 0, sizeof(_chan_name_filters)); +#if defined(RP2040_PLATFORM) + File f = _fs->open(fname, "r"); +#else + File f = _fs->open(fname); +#endif + if (!f) return; + char line[40]; + int line_len = 0; + while (f.available() && _num_chan_name_filters < MAX_CHAN_NAME_FILTERS) { + int c = f.read(); + if (c < 0) break; + if (c == '\n' || c == '\r') { + if (line_len > 0 && line[0] == '#') { + line[line_len] = 0; + deriveChanNameFilter(_chan_name_filters[_num_chan_name_filters], line); + _num_chan_name_filters++; + } + line_len = 0; + } else if (line_len < (int)(sizeof(line) - 1)) { + line[line_len++] = (char)c; + } + } + // handle last line with no trailing newline + if (line_len > 0 && line[0] == '#' && _num_chan_name_filters < MAX_CHAN_NAME_FILTERS) { + line[line_len] = 0; + deriveChanNameFilter(_chan_name_filters[_num_chan_name_filters], line); + _num_chan_name_filters++; + } + f.close(); +} + +void MyMesh::saveChanBlacklist(const char* fname) { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + _fs->remove(fname); + File f = _fs->open(fname, FILE_O_WRITE); +#elif defined(RP2040_PLATFORM) + File f = _fs->open(fname, "w"); +#else + File f = _fs->open(fname, "w", true); +#endif + if (!f) return; + // Write hex prefix entries + for (int i = 0; i < MAX_BLACKLIST_ENTRIES; i++) { + if (_chan_blacklist[i].len == 0) continue; + char hex[MAX_PATH_PREFIX_LEN * 2 + 2]; + mesh::Utils::toHex(hex, _chan_blacklist[i].prefix, _chan_blacklist[i].len); + f.println(hex); + } + // Write #channel_name entries + for (int i = 0; i < _num_chan_name_filters; i++) { + f.println(_chan_name_filters[i].name); + } + f.close(); +} + +void MyMesh::formatChanBlacklist(char* reply) { + char* dp = reply; + char* end = reply + MAX_PACKET_PAYLOAD - 6; // leave room for null + safety + int count = 0; + // Format hex prefix entries + for (int i = 0; i < MAX_BLACKLIST_ENTRIES; i++) { + if (_chan_blacklist[i].len == 0) continue; + if (dp + _chan_blacklist[i].len * 2 + 1 >= end) break; + if (count > 0) *dp++ = '\n'; + mesh::Utils::toHex(dp, _chan_blacklist[i].prefix, _chan_blacklist[i].len); + dp += _chan_blacklist[i].len * 2; + count++; + } + // Format #channel_name entries + for (int i = 0; i < _num_chan_name_filters; i++) { + int len = strlen(_chan_name_filters[i].name); + if (dp + len + 1 >= end) break; + if (count > 0) *dp++ = '\n'; + memcpy(dp, _chan_name_filters[i].name, len); + dp += len; + count++; + } + if (count == 0) { + strcpy(reply, "-none-"); + } else { + *dp = 0; + } +} + mesh::DispatcherAction MyMesh::onRecvPacket(mesh::Packet* pkt) { if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD); @@ -906,6 +1204,20 @@ mesh::DispatcherAction MyMesh::onRecvPacket(mesh::Packet* pkt) { } else { recv_pkt_region = NULL; } + + if (pkt->isRouteFlood()) { + // Drop flood packets whose path contains a blacklisted node prefix + if (isPathBlacklisted(pkt)) { + MESH_DEBUG_PRINTLN("onRecvPacket: path prefix blacklisted, dropping!"); + return ACTION_RELEASE; + } + // Drop group-channel flood packets whose channel hash is blacklisted + if (isChanBlacklisted(pkt)) { + MESH_DEBUG_PRINTLN("onRecvPacket: channel hash blacklisted, dropping!"); + return ACTION_RELEASE; + } + } + return Mesh::onRecvPacket(pkt); } @@ -1239,6 +1551,11 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc memset(&_stats_state, 0, sizeof(_stats_state)); recv_pkt_region = NULL; + memset(_path_blacklist, 0, sizeof(_path_blacklist)); + memset(_chan_blacklist, 0, sizeof(_chan_blacklist)); + memset(_chan_name_filters, 0, sizeof(_chan_name_filters)); + _num_chan_name_filters = 0; + #if MAX_NEIGHBOURS memset(neighbours, 0, sizeof(neighbours)); #endif @@ -1304,6 +1621,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) { mesh::Mesh::begin(); +#if defined(ESP32) + _cpu_tracker.begin(); +#endif _fs = fs; _archive = archive; last_millis = millis(); @@ -1312,6 +1632,8 @@ void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) { acl.load(_fs, self_id); // TODO: key_store.begin(); region_map.load(_fs); + loadBlacklist(PATH_BLACKLIST_FILE, _path_blacklist); + loadChanBlacklist(CHAN_BLACKLIST_FILE); // establish default-scope { @@ -1580,9 +1902,10 @@ void MyMesh::removeNeighbor(const uint8_t *pubkey, int key_len) { void MyMesh::formatStatsReply(char *reply, size_t reply_size) { snprintf(reply, reply_size, - "{\"battery_mv\":%u,\"uptime_secs\":%u,\"errors\":%u,\"queue_len\":%u}", + "{\"battery_mv\":%u,\"uptime_secs\":%u,\"core0_util\":%.1f,\"errors\":%u,\"queue_len\":%u}", getBatteryMilliVolts(true), _ms->getMillis() / 1000, + _cpu_tracker.getCore0Util() * 100.0f, _err_flags, _mgr->getOutboundTotal()); } @@ -2059,6 +2382,7 @@ void MyMesh::updateStatsHistory(unsigned long now_ms) { sample.heap_min = ESP.getMinFreeHeap(); sample.psram_free = ESP.getFreePsram(); sample.psram_min = ESP.getMinFreePsram(); + sample.core0_util_pct = (uint8_t)(_cpu_tracker.getCore0Util() * 100.0f + 0.5f); #endif if (board.isExternalPowered()) sample.flags |= HISTORY_FLAG_EXTERNAL_POWER; if (board.isCharging()) sample.flags |= HISTORY_FLAG_CHARGING; @@ -2373,6 +2697,22 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply sprintf(reply, "> %s", network.getNtpServer(3)); #endif #if defined(ESP_PLATFORM) && WITH_WEB_PANEL + } else if (strcmp(command, "web cert status") == 0) { + web.formatCertStatusReply(reply, 160); + } else if (strcmp(command, "web cert reset") == 0) { + strcpy(reply, web.resetCert() ? "OK - built-in certificate restored, panel restarting" : "Err - reset failed"); + } else if (strcmp(command, "web cert begin") == 0) { + strcpy(reply, web.certInputBegin() ? "OK - send PEM lines with 'web cert add ', then 'web cert end'" + : "Err - out of memory"); + } else if (memcmp(command, "web cert add ", 13) == 0) { + strcpy(reply, web.certInputAdd(&command[13]) ? "OK" : "Err - not started or buffer full (12 KB)"); + } else if (strcmp(command, "web cert end") == 0) { + char cert_err[96]; + if (web.certInputEnd(cert_err, sizeof(cert_err))) { + strcpy(reply, "OK - certificate installed, panel restarting"); + } else { + snprintf(reply, 160, "Err - %s", cert_err); + } } else if (memcmp(command, "set web ", 8) == 0) { web.setWebEnabled(memcmp(&command[8], "on", 2) == 0); strcpy(reply, "OK"); @@ -2475,16 +2815,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply sprintf(reply, "> %s", mqtt.isRawEnabled() ? "on" : "off"); } else if (strcmp(command, "get mqtt.tx") == 0) { sprintf(reply, "> %s", mqtt.isTxEnabled() ? "on" : "off"); - } else if (strcmp(command, "get mqtt.eastmesh-au") == 0 || strcmp(command, "get mqtt.eastmesh.au") == 0) { + } else if (strcmp(command, "get mqtt.meshcoretel") == 0 || strcmp(command, "get mqtt.meshcoretel.ru") == 0) { sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x01) ? "on" : "off"); - } else if (strcmp(command, "get mqtt.meshmapper") == 0) { - sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x10) ? "on" : "off"); - } else if (strcmp(command, "get mqtt.waev") == 0) { - sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x20) ? "on" : "off"); - } else if (strcmp(command, "get mqtt.letsmesh-eu") == 0 || strcmp(command, "get mqtt.letsmesh.eu") == 0) { - sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x02) ? "on" : "off"); - } else if (strcmp(command, "get mqtt.letsmesh-us") == 0 || strcmp(command, "get mqtt.letsmesh.us") == 0) { - sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x04) ? "on" : "off"); } else if (strcmp(command, "get mqtt.custom") == 0) { sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x08) ? "on" : "off"); } else if (strcmp(command, "get mqtt.custom.host") == 0) { @@ -2527,32 +2859,11 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else if (memcmp(command, "set mqtt.status ", 16) == 0) { mqtt.setStatusEnabled(memcmp(&command[16], "on", 2) == 0); strcpy(reply, "OK"); - } else if (memcmp(command, "set mqtt.eastmesh-au ", 21) == 0 || memcmp(command, "set mqtt.eastmesh.au ", 21) == 0) { - if (mqtt.setEndpointEnabled(0x01, memcmp(&command[21], "on", 2) == 0)) { - strcpy(reply, "OK"); - } else { - strcpy(reply, "Err - max 2 mqtt brokers"); - } - } else if (memcmp(command, "set mqtt.meshmapper ", 20) == 0) { - if (mqtt.setEndpointEnabled(0x10, memcmp(&command[20], "on", 2) == 0)) { - strcpy(reply, "OK"); - } else { - strcpy(reply, "Err - max 2 mqtt brokers"); - } - } else if (memcmp(command, "set mqtt.waev ", 14) == 0) { - if (mqtt.setEndpointEnabled(0x20, memcmp(&command[14], "on", 2) == 0)) { - strcpy(reply, "OK"); - } else { - strcpy(reply, "Err - max 2 mqtt brokers"); - } - } else if (memcmp(command, "set mqtt.letsmesh-eu ", 21) == 0 || memcmp(command, "set mqtt.letsmesh.eu ", 21) == 0) { - if (mqtt.setEndpointEnabled(0x02, memcmp(&command[21], "on", 2) == 0)) { - strcpy(reply, "OK"); - } else { - strcpy(reply, "Err - max 2 mqtt brokers"); - } - } else if (memcmp(command, "set mqtt.letsmesh-us ", 21) == 0 || memcmp(command, "set mqtt.letsmesh.us ", 21) == 0) { - if (mqtt.setEndpointEnabled(0x04, memcmp(&command[21], "on", 2) == 0)) { + } else if (memcmp(command, "set mqtt.meshcoretel ", 21) == 0 || memcmp(command, "set mqtt.meshcoretel.ru ", 24) == 0) { + // The ".ru" alias is three characters longer, so the on/off value does not always + // start at the same offset. + const int value_offset = (command[20] == '.') ? 24 : 21; + if (mqtt.setEndpointEnabled(0x01, memcmp(&command[value_offset], "on", 2) == 0)) { strcpy(reply, "OK"); } else { strcpy(reply, "Err - max 2 mqtt brokers"); @@ -2564,36 +2875,103 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply strcpy(reply, "Err - max 2 mqtt brokers"); } } else if (memcmp(command, "set mqtt.custom.host ", 21) == 0) { - if (mqtt.setCustomHost(&command[21])) { - strcpy(reply, "OK"); - } else { - strcpy(reply, "Err - bad mqtt.custom.host"); - } + strcpy(reply, mqtt.setCustomHost(&command[21]) ? "OK" : "Err - bad mqtt.custom.host"); } else if (memcmp(command, "set mqtt.custom.port ", 21) == 0) { - if (mqtt.setCustomPort(&command[21])) { - strcpy(reply, "OK"); - } else { - strcpy(reply, "Err - bad mqtt.custom.port"); - } + strcpy(reply, mqtt.setCustomPort(&command[21]) ? "OK" : "Err - bad mqtt.custom.port"); } else if (memcmp(command, "set mqtt.custom.transport ", 26) == 0) { - if (mqtt.setCustomTransport(&command[26])) { - strcpy(reply, "OK"); - } else { - strcpy(reply, "Err - bad mqtt.custom.transport"); - } + strcpy(reply, mqtt.setCustomTransport(&command[26]) ? "OK" : "Err - use tcp|wss"); } else if (memcmp(command, "set mqtt.custom.username ", 25) == 0) { - if (mqtt.setCustomUsername(&command[25])) { - strcpy(reply, "OK"); - } else { - strcpy(reply, "Err - bad mqtt.custom.username"); - } + strcpy(reply, mqtt.setCustomUsername(&command[25]) ? "OK" : "Err - bad mqtt.custom.username"); } else if (memcmp(command, "set mqtt.custom.password ", 25) == 0) { - if (mqtt.setCustomPassword(&command[25])) { - strcpy(reply, "OK"); - } else { - strcpy(reply, "Err - bad mqtt.custom.password"); - } + strcpy(reply, mqtt.setCustomPassword(&command[25]) ? "OK" : "Err - bad mqtt.custom.password"); #endif + } else if (memcmp(command, "blacklist ", 10) == 0) { + // Commands: + // blacklist path list + // blacklist path add [,,...] (each hex = 2-8 even chars > 1-4 bytes) + // blacklist path rem [,,...] + // blacklist path clear + // blacklist chan list + // blacklist chan add [,,...] + // blacklist chan rem [,,...] + // blacklist chan clear + const char* parts[5]; + int n = mesh::Utils::parseTextParts(command, parts, 5, ' '); + + BlacklistEntry* list = NULL; + const char* list_file = NULL; + bool is_chan = false; + if (n >= 2 && strcmp(parts[1], "path") == 0) { + list = _path_blacklist; list_file = PATH_BLACKLIST_FILE; + } else if (n >= 2 && strcmp(parts[1], "chan") == 0) { + list = _chan_blacklist; list_file = CHAN_BLACKLIST_FILE; + is_chan = true; + } + + if (list && n >= 3 && strcmp(parts[2], "list") == 0) { + if (is_chan) { + formatChanBlacklist(reply); + } else { + formatBlacklist(list, reply); + } + } else if (list && n >= 3 && strcmp(parts[2], "clear") == 0) { + memset(list, 0, sizeof(BlacklistEntry) * MAX_BLACKLIST_ENTRIES); + if (is_chan) { + _num_chan_name_filters = 0; + memset(_chan_name_filters, 0, sizeof(_chan_name_filters)); + saveChanBlacklist(list_file); + } else { + saveBlacklist(list_file, list); + } + strcpy(reply, "OK"); + } else if (list && n >= 4 && (strcmp(parts[2], "add") == 0 || strcmp(parts[2], "rem") == 0)) { + bool is_add = (parts[2][0] == 'a'); + // parts[3] may be a comma-separated list of hex entries or #channel_name entries + char tokens[MAX_PATH_PREFIX_LEN * 2 * MAX_BLACKLIST_ENTRIES + MAX_BLACKLIST_ENTRIES + 2]; + strncpy(tokens, parts[3], sizeof(tokens) - 1); + tokens[sizeof(tokens) - 1] = 0; + + const char* tok_parts[MAX_BLACKLIST_ENTRIES]; + int tok_n = mesh::Utils::parseTextParts(tokens, tok_parts, MAX_BLACKLIST_ENTRIES, ','); + + bool any_ok = false, any_err = false; + for (int t = 0; t < tok_n; t++) { + if (is_chan && tok_parts[t][0] == '#') { + // #channel_name entry + bool ok = is_add ? addChanNameFilter(tok_parts[t]) + : removeChanNameFilter(tok_parts[t]); + if (ok) any_ok = true; else any_err = true; + } else { + int hex_str_len = strlen(tok_parts[t]); + if (hex_str_len < 2 || hex_str_len > MAX_PATH_PREFIX_LEN * 2 || (hex_str_len % 2) != 0) { + any_err = true; continue; + } + uint8_t prefix[MAX_PATH_PREFIX_LEN]; + int byte_len = hex_str_len / 2; + if (!mesh::Utils::fromHex(prefix, byte_len, tok_parts[t])) { + any_err = true; continue; + } + bool ok = is_add ? addToBlacklist(list, prefix, (uint8_t)byte_len) + : removeFromBlacklist(list, prefix, (uint8_t)byte_len); + if (ok) any_ok = true; else any_err = true; + } + } + + // auto-save on any successful mutation + if (any_ok) { + if (is_chan) { + saveChanBlacklist(list_file); + } else { + saveBlacklist(list_file, list); + } + } + + if (any_ok && !any_err) strcpy(reply, "OK"); + else if (any_ok && any_err) strcpy(reply, "OK (partial)"); + else strcpy(reply, is_add ? "Err - list full or bad input" : "Err - not found or bad input"); + } else { + strcpy(reply, "Err - usage: blacklist [hex|#name[,...]]"); + } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands } @@ -2778,7 +3156,7 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) { "\"archive\":{\"logical\":\"%s\",\"available\":%s,\"path\":\"%s\",\"type\":\"%s\"," "\"total_bytes\":%llu,\"used_bytes\":%llu}," "\"core\":{\"battery_mv\":%u,\"battery_pct\":%d,\"battery_display_pct\":%d,\"battery_min_mv\":%u,\"battery_max_mv\":%u," - "\"uptime_secs\":%lu,\"errors\":%u,\"queue_len\":%u," + "\"uptime_secs\":%lu,\"core0_util\":%.1f,\"errors\":%u,\"queue_len\":%u," "\"external_power\":%s,\"charging\":%s,\"vbus\":%s}," "\"radio\":{\"noise_floor\":%d,\"last_rssi\":%.2f,\"last_snr\":%.2f,\"tx_air_secs\":%lu,\"rx_air_secs\":%lu}," "\"packets\":{\"recv\":%u,\"sent\":%u,\"flood_tx\":%u,\"direct_tx\":%u,\"flood_rx\":%u,\"direct_rx\":%u," @@ -2812,6 +3190,7 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) { battery_min_mv, battery_max_mv, static_cast(uptime_millis / 1000), + _cpu_tracker.getCore0Util() * 100.0f, _err_flags, static_cast(_mgr->getOutboundTotal()), board.isExternalPowered() ? "true" : "false", diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index f0af95ad..ef722a20 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -11,6 +11,7 @@ #include #elif defined(ESP32) #include + #include "../../arch/esp32/CPUUsageTracker.h" using File = fs::File; #endif @@ -111,6 +112,21 @@ struct WebSensorSnapshot { #define MAX_CLIENTS 32 #endif +#define MAX_PATH_PREFIX_LEN 4 +#define MAX_BLACKLIST_ENTRIES 64 +#define MAX_CHAN_NAME_FILTERS 32 + +struct BlacklistEntry { + uint8_t len; // 0 = empty slot + uint8_t prefix[MAX_PATH_PREFIX_LEN]; +}; + +struct ChanNameFilter { + uint8_t hash[PATH_HASH_SIZE]; + uint8_t secret[PUB_KEY_SIZE]; + char name[32]; +}; + struct NeighbourInfo { mesh::Identity id; uint32_t advert_timestamp; @@ -123,7 +139,7 @@ struct NeighbourInfo { #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.17.1" + #define FIRMWARE_VERSION "v1.17.1-posdamesh-meshcoretel" #endif #define FIRMWARE_ROLE "repeater" @@ -162,6 +178,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelComm unsigned long pending_discover_until; bool region_load_active; unsigned long dirty_contacts_expiry; + BlacklistEntry _path_blacklist[MAX_BLACKLIST_ENTRIES]; + BlacklistEntry _chan_blacklist[MAX_BLACKLIST_ENTRIES]; + ChanNameFilter _chan_name_filters[MAX_CHAN_NAME_FILTERS]; + int _num_chan_name_filters; #if MAX_NEIGHBOURS NeighbourInfo neighbours[MAX_NEIGHBOURS]; #endif @@ -187,6 +207,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelComm #endif #if defined(ESP_PLATFORM) && WITH_WEB_PANEL WebService web; +#endif +#if defined(ESP32) + CPUUsageTracker _cpu_tracker; #endif StatsHistory _stats_history; struct { @@ -222,6 +245,20 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelComm File openAppend(const char* fname); bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]); + bool isPathBlacklisted(const mesh::Packet* packet) const; + bool isChanBlacklisted(const mesh::Packet* packet) const; + void loadBlacklist(const char* fname, BlacklistEntry* list); + void saveBlacklist(const char* fname, const BlacklistEntry* list); + bool addToBlacklist(BlacklistEntry* list, const uint8_t* prefix, uint8_t len); + bool removeFromBlacklist(BlacklistEntry* list, const uint8_t* prefix, uint8_t len); + void formatBlacklist(const BlacklistEntry* list, char* reply); + void deriveChanNameFilter(ChanNameFilter& entry, const char* name); + bool addChanNameFilter(const char* name); + bool removeChanNameFilter(const char* name); + void loadChanBlacklist(const char* fname); + void saveChanBlacklist(const char* fname); + void formatChanBlacklist(char* reply); + protected: float getAirtimeBudgetFactor() const override { return _prefs.airtime_factor; @@ -324,6 +361,11 @@ public: void handleCommand(uint32_t sender_timestamp, char* command, char* reply); void runWebCommand(const char* command, char* reply, size_t reply_size) override; const char* getWebAdminPassword() const override { return _prefs.password; } +#if defined(ESP_PLATFORM) && WITH_WEB_PANEL + bool installWebCertBundle(const char* pem, size_t len, char* err, size_t err_size) override { + return web.installCertBundle(pem, len, err, err_size); + } +#endif bool isWebStatsEnabled() const override; bool formatWebStatsSummaryJson(char* reply, size_t reply_size) override; bool formatWebStatsSeriesJson(const char* series, char* reply, size_t reply_size) override; diff --git a/mkdocs.yml b/mkdocs.yml index b99fb5f8..99496904 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,11 +1,10 @@ -site_name: MeshCore EastMesh Docs -site_url: https://xjarid.github.io/MeshCore-EastMesh/ -site_description: Documentation for the EastMesh layer on top of MeshCore, including WiFi, MQTT, and release workflows +site_name: Документация MeshCore PosadMesh +site_description: "Документация слоя PosadMesh поверх MeshCore — Wi-Fi, MQTT и процесс релизов" -repo_name: xJARiD/MeshCore-EastMesh -repo_url: https://github.com/xJARiD/MeshCore-EastMesh -edit_uri: edit/main/eastmesh-docs/ -docs_dir: eastmesh-docs +repo_name: shade/MeshCore-Posadmesh +repo_url: https://git.meshinfo.ru/shade/MeshCore-Posadmesh +edit_uri: _edit/main/posadmesh-docs/ +docs_dir: posadmesh-docs theme: name: material @@ -23,24 +22,24 @@ theme: accent: custom toggle: icon: material/weather-night - name: Switch to dark mode + name: Переключить на тёмную тему - media: "(prefers-color-scheme: dark)" scheme: slate primary: custom accent: custom toggle: icon: material/weather-sunny - name: Switch to light mode + name: Переключить на светлую тему extra_css: - _stylesheets/extra.css nav: - - Home: index.md - - Boards: boards.md - - Releases: releases.md - - Migration: migration.md - - Web Panel: web-panel.md - - Repeater API: api.md - - Local Builds: local-builds.md - - Custom CLI: custom-cli.md + - Главная: index.md + - Платы: boards.md + - Релизы: releases.md + - Миграция: migration.md + - Веб-панель: web-panel.md + - API репитера: api.md + - Локальные сборки: local-builds.md + - Кастомный CLI: custom-cli.md diff --git a/platformio.ini b/platformio.ini index f7625551..a4a3e215 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,6 +26,7 @@ lib_deps = melopero/Melopero RV3028 @ ^1.1.0 electroniccats/CayenneLPP @ 1.6.1 build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1 + -D RADIOLIB_STATIC_ARRAY_SIZE=260 -D LORA_FREQ=869.618 -D LORA_BW=62.5 -D LORA_SF=8 @@ -61,11 +62,17 @@ platform = platformio/espressif32@6.11.0 monitor_filters = esp32_exception_decoder extra_scripts = pre:arch/esp32/extra_scripts/generate_web_panel_cert.py + pre:arch/esp32/extra_scripts/git_commit_hash.py merge-bin.py build_flags = ${arduino_base.build_flags} -D ESP32_PLATFORM ; -D ESP32_CPU_FREQ=80 ; change it to your need + -Wl,--wrap=xTaskCreatePinnedToCore + -Wl,--wrap=mbedtls_ssl_config_defaults build_src_filter = ${arduino_base.build_src_filter} + +<../arch/esp32/task_pinning.c> + +<../arch/esp32/tls_server_config.c> + +<../arch/esp32/CPUUsageTracker.cpp> [esp32_ota] lib_deps = diff --git a/eastmesh-build.sh b/posadmesh-build.sh similarity index 91% rename from eastmesh-build.sh rename to posadmesh-build.sh index b27002c6..89aa66b1 100755 --- a/eastmesh-build.sh +++ b/posadmesh-build.sh @@ -23,7 +23,7 @@ run_pio() { global_usage() { cat - < [target] +sh posadmesh-build.sh [target] Commands: help|usage|-h|--help: Shows this message. @@ -42,34 +42,34 @@ Commands: Examples: Build firmware for the "RAK_4631_repeater" device target -$ sh eastmesh-build.sh build-firmware RAK_4631_repeater +$ sh posadmesh-build.sh build-firmware RAK_4631_repeater Build all firmwares for device targets containing the string "RAK_4631" -$ sh eastmesh-build.sh build-matching-firmwares +$ sh posadmesh-build.sh build-matching-firmwares Build all companion firmwares -$ sh eastmesh-build.sh build-companion-firmwares +$ sh posadmesh-build.sh build-companion-firmwares Build all companion WiFi firmwares -$ sh eastmesh-build.sh build-companion-wifi-firmwares +$ sh posadmesh-build.sh build-companion-wifi-firmwares Build all repeater firmwares -$ sh eastmesh-build.sh build-repeater-firmwares +$ sh posadmesh-build.sh build-repeater-firmwares Build all repeater ESP-NOW bridge firmwares -$ sh eastmesh-build.sh build-repeater-bridge-espnow-firmwares +$ sh posadmesh-build.sh build-repeater-bridge-espnow-firmwares Build all observer firmwares -$ sh eastmesh-build.sh build-observer-firmwares +$ sh posadmesh-build.sh build-observer-firmwares Build all observer ESP-NOW firmwares -$ sh eastmesh-build.sh build-observer-espnow-firmwares +$ sh posadmesh-build.sh build-observer-espnow-firmwares Build all observer MQTT bridge firmwares -$ sh eastmesh-build.sh build-observer-mqtt-bridge-firmwares +$ sh posadmesh-build.sh build-observer-mqtt-bridge-firmwares Build all chat room server firmwares -$ sh eastmesh-build.sh build-room-server-firmwares +$ sh posadmesh-build.sh build-room-server-firmwares Environment Variables: DISABLE_DEBUG=1: Disables all debug logging flags (MESH_DEBUG, MESH_PACKET_LOGGING, etc.) @@ -79,11 +79,11 @@ Examples: Build without debug logging: $ export FIRMWARE_VERSION=v1.0.0 $ export DISABLE_DEBUG=1 -$ sh eastmesh-build.sh build-firmware RAK_4631_repeater +$ sh posadmesh-build.sh build-firmware RAK_4631_repeater Build with debug logging (default, uses flags from variant files): $ export FIRMWARE_VERSION=v1.0.0 -$ sh eastmesh-build.sh build-firmware RAK_4631_repeater +$ sh posadmesh-build.sh build-firmware RAK_4631_repeater EOF } @@ -204,15 +204,15 @@ build_firmware() { fi # set artifact version string - # e.g: v1.2.3-eastmesh-v1.0.1-abcdef + # e.g: v1.2.3-posadmesh-v1.0.1-abcdef FIRMWARE_VERSION_BASE="${FIRMWARE_VERSION}" - if [ -n "$EASTMESH_VERSION" ]; then - FIRMWARE_VERSION_BASE="${FIRMWARE_VERSION_BASE}-eastmesh-${EASTMESH_VERSION}" + if [ -n "$POSADMESH_VERSION" ]; then + FIRMWARE_VERSION_BASE="${FIRMWARE_VERSION_BASE}-posadmesh-${POSADMESH_VERSION}" fi FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION_BASE}-${COMMIT_HASH}" - CLIENT_VERSION_STRING="EastMesh/${COMMIT_HASH}" - if [ -n "$EASTMESH_VERSION" ]; then - CLIENT_VERSION_STRING="EastMesh/${EASTMESH_VERSION}-${COMMIT_HASH}" + CLIENT_VERSION_STRING="PosadMesh/${COMMIT_HASH}" + if [ -n "$POSADMESH_VERSION" ]; then + CLIENT_VERSION_STRING="PosadMesh/${POSADMESH_VERSION}-${COMMIT_HASH}" fi # craft filename diff --git a/eastmesh-docs/_assets/repeater_web_panel_app_dark.png b/posadmesh-docs/_assets/repeater_web_panel_app_dark.png similarity index 100% rename from eastmesh-docs/_assets/repeater_web_panel_app_dark.png rename to posadmesh-docs/_assets/repeater_web_panel_app_dark.png diff --git a/eastmesh-docs/_assets/repeater_web_panel_app_light.png b/posadmesh-docs/_assets/repeater_web_panel_app_light.png similarity index 100% rename from eastmesh-docs/_assets/repeater_web_panel_app_light.png rename to posadmesh-docs/_assets/repeater_web_panel_app_light.png diff --git a/eastmesh-docs/_assets/repeater_web_panel_stats_dark.png b/posadmesh-docs/_assets/repeater_web_panel_stats_dark.png similarity index 100% rename from eastmesh-docs/_assets/repeater_web_panel_stats_dark.png rename to posadmesh-docs/_assets/repeater_web_panel_stats_dark.png diff --git a/eastmesh-docs/_assets/repeater_web_panel_stats_light.png b/posadmesh-docs/_assets/repeater_web_panel_stats_light.png similarity index 100% rename from eastmesh-docs/_assets/repeater_web_panel_stats_light.png rename to posadmesh-docs/_assets/repeater_web_panel_stats_light.png diff --git a/eastmesh-docs/_assets/xjarid.svg b/posadmesh-docs/_assets/xjarid.svg similarity index 100% rename from eastmesh-docs/_assets/xjarid.svg rename to posadmesh-docs/_assets/xjarid.svg diff --git a/eastmesh-docs/_stylesheets/extra.css b/posadmesh-docs/_stylesheets/extra.css similarity index 100% rename from eastmesh-docs/_stylesheets/extra.css rename to posadmesh-docs/_stylesheets/extra.css diff --git a/posadmesh-docs/api.md b/posadmesh-docs/api.md new file mode 100644 index 00000000..f548c848 --- /dev/null +++ b/posadmesh-docs/api.md @@ -0,0 +1,410 @@ +# Веб-API репитера + +Эта страница описывает локальный HTTPS API, который предоставляют сборки PosadMesh `*_repeater_observer` с поддержкой веб-панели. + +Он предназначен для: + +- небольшой автоматизации в вашей локальной сети +- дашбордов или скриптов, которым нужен текущий статус репитера +- удалённого доступа к CLI через тот же аутентифицированный путь, который использует веб-панель + +Это не облачный API и не отдельный серверный сервис. Его напрямую обслуживает прошивка репитера. + +## С чего начать + +Для обычного использования PosadMesh эта страница не нужна. Обратитесь к [Веб-панели репитера](./web-panel.md), если вы просто хотите настроить или проверить репитер в браузере. + +Используйте эту страницу, когда нужно, чтобы локальный скрипт, дашборд или домашний инструмент общался с репитером напрямую. + +Простейший полезный сценарий работы с API: + +1. войдите с паролем администратора репитера +2. сохраните полученный токен +3. отправьте команду CLI через `/api/command` + +```bash +TOKEN=$(curl -sk -X POST https:///login --data '') + +curl -sk https:///api/command \ + -H "X-Auth-Token: $TOKEN" \ + --data 'get mqtt.status' +``` + +## Область применения и доступность + +API доступен только когда: + +- вы запускаете поддерживаемую сборку прошивки `*_repeater_observer` +- веб-панель репитера включена и работает +- репитер доступен из локальной сети +- вы прошли аутентификацию с паролем администратора репитера + +API предназначен для администрирования в доверенной локальной сети. Не открывайте его напрямую в публичный интернет. + +## Базовый URL + +Используйте локальный HTTPS-адрес репитера: + +```text +https:/// +``` + +Пример: + +```text +https://192.168.1.123/ +``` + +## Аутентификация + +API использует тот же пароль администратора, что и CLI репитера и веб-панель. + +1. отправьте `POST` с паролем на `/login` +2. сохраните полученный токен сессии +3. передавайте этот токен в заголовке `X-Auth-Token` в последующих запросах + +Пример: + +```bash +TOKEN=$(curl -sk -X POST https:///login --data '') +``` + +Использование токена: + +```bash +curl -sk https:///api/stats -H "X-Auth-Token: $TOKEN" +``` + +Примечания: + +- репитер использует самоподписанный сертификат, поэтому большинству инструментов потребуется `-k` или аналог +- если сессия истекает или заблокирована, запросы возвращают `401 Unauthorized` +- повторный вход выдаёт новый токен + +## Рекомендации по производительности + +API работает на самом репитере, поэтому частота опроса имеет значение. + +Если репитер дополнительно держит два MQTT-соединения, избегайте частого опроса API. Текущая схема использования в PosadMesh: + +- опрос статистики раз в `60` секунд +- запросы по требованию для всего остального + +Это рекомендуемая базовая схема, если вы хотите избежать перегрузки платы. Держите частоту запросов низкой, избегайте всплесков опроса и предпочитайте ручное обновление или чтение по событию для более тяжёлых операций. + +Рекомендуемая практика: + +- опрашивайте `/api/stats` не чаще одного раза в минуту +- не опрашивайте несколько эндпоинтов параллельно +- используйте вызовы по требованию для чтения конфигурации и действий CLI +- завершайте сессию, когда закончили, и прекращайте опрос, когда данные активно не используются + +## Эндпоинты + +### `POST /login` + +Аутентификация с паролем администратора репитера. + +Тело запроса: + +```text + +``` + +Ответ: + +- токен сессии в виде обычного текста при успехе +- `401` при неверном пароле + +Пример: + +```bash +curl -sk -X POST https:///login --data '' +``` + +### `GET /api/session` + +Проверка, действителен ли сохранённый токен сессии. + +Заголовки: + +```text +X-Auth-Token: +``` + +Ответ: + +- JSON `{"authenticated":true}` при успехе +- `401 Unauthorized`, если токен отсутствует или устарел после блокировки, перезапуска или перепрошивки + +### `POST /api/command` + +Удалённый запуск команды CLI репитера. + +Заголовки: + +```text +X-Auth-Token: +``` + +Тело запроса: + +```text +get wifi.status +``` + +Ответ: + +- вывод CLI в виде обычного текста +- `OK`, если команда выполняется успешно и не возвращает текст + +Пример: + +```bash +curl -sk https:///api/command \ + -H "X-Auth-Token: $TOKEN" \ + --data 'get wifi.status' +``` + +### `POST /api/firmware-update` + +Загрузите `.bin` прошивки приложения через HTTPS-веб-панель и перезагрузитесь после успешного обновления. + +Заголовки: + +```text +X-Auth-Token: +X-Firmware-MD5: <32-character md5> +Content-Type: application/octet-stream +``` + +Тело запроса: + +```text +raw firmware .bin bytes +``` + +Ответ: + +- `OK` после того, как прошивка записана и перезагрузка запланирована +- `401 Unauthorized`, если токен отсутствует или устарел +- `400` или `500`, если загрузка или проверка прошивки не удалась + +### `GET /api/stats` + +Получить текущий сводный payload, который использует отдельная страница `/stats`. + +Заголовки: + +```text +X-Auth-Token: +``` + +Пример: + +```bash +curl -sk https:///api/stats \ + -H "X-Auth-Token: $TOKEN" +``` + +Примечания: + +- именно это сводное представление веб-панель из репозитория запрашивает первым, прежде чем загружать серии трендов +- если `web.stats` отключён, эндпоинт возвращает `503 Service Unavailable` +- поддерживаемые платы могут также включать необязательный объект `sensors` в сводный payload для текущей телеметрии GPS и окружающей среды +- объект `core` включает необработанный `battery_mv`, сообщаемый платой `battery_pct`, когда он доступен, готовый для UI `battery_display_pct` и специфичные для платы подсказки диапазона `battery_min_mv` / `battery_max_mv`, которые использует `/stats`, когда плата не предоставляет собственный процент заряда батареи + +### `GET /api/stats?series=` + +Получить одну серию тренда. + +Поддерживаемые серии: + +- `battery` +- `memory` +- `signal` +- `noise_floor` +- `packets` +- `voltage` +- `sensor_temp` +- `humidity` +- `pressure` +- `pressure_altitude` +- `mcu_temp` +- `gps_altitude` +- `gps_satellites` + +Пример: + +```bash +curl -sk "https:///api/stats?series=memory" \ + -H "X-Auth-Token: $TOKEN" +``` + +Примечания: + +- используйте `?series=battery`, а не просто `?series` +- встроенная веб-панель загружает эти серии последовательно, а не все сразу, чтобы снизить нагрузку на память платы +- серии окружающей среды включаются только когда плата сообщает эти показания; если в серии ещё нет сохранённых точек, она возвращает пустой массив `points` и `current:null` + +### `GET /api/stats?view=legacy` + +Получить более старый payload статистики в стиле bundle. + +Пример: + +```bash +curl -sk "https:///api/stats?view=legacy" \ + -H "X-Auth-Token: $TOKEN" +``` + +Он существует для совместимости и диагностики. Для новых интеграций предпочитайте сводный эндпоинт плюс отдельные запросы `series`. + +## Типовые сценарии использования + +### 1. Удалённый доступ к CLI + +`/api/command` — самый гибкий эндпоинт. Он позволяет выполнять те же команды CLI, которые принимает репитер. + +Примеры: + +- `get wifi.status` +- `get mqtt.status` +- `get web.status` +- `get web.stats.status` +- `get repeat` +- `get radio` + +Это полезно для: + +- удалённой диагностики с ноутбука или телефона +- простых скриптов, которые собирают рабочее состояние +- админ-инструментов, которые хотят переиспользовать существующее поведение CLI вместо добавления новых эндпоинтов прошивки + +Пример: + +```bash +curl -sk https:///api/command \ + -H "X-Auth-Token: $TOKEN" \ + --data 'get mqtt.status' +``` + +### 2. Сборка лёгкого дашборда состояния + +Используйте `/api/stats` для сводной информации и по одному вызову `series` за раз для линий трендов. + +Рекомендуемый сценарий: + +1. получите `/api/stats` +2. отобразите текущее состояние сервисов и сводные поля +3. запрашивайте одну серию тренда только когда это нужно +4. обновляйте с интервалом `60` секунд или реже, если репитер загружен + +Это тот же базовый сценарий, который использует встроенная страница `/stats`. + +### 3. Использование API для быстрых проверок работоспособности + +Поскольку `/api/command` возвращает вывод CLI напрямую, он хорошо подходит для небольших эксплуатационных проверок в скриптах или домашнем мониторинге. + +Примеры: + +- убедиться, что у репитера по-прежнему есть Wi-Fi +- проверить состояние подключения к MQTT-брокеру +- убедиться, что веб-панель включена, прежде чем пытаться читать статистику +- убедиться в текущих настройках радио перед применением изменений + +Пример: + +```bash +curl -sk https:///api/command \ + -H "X-Auth-Token: $TOKEN" \ + --data 'get web.status' +``` + +### 4. Удалённые административные действия + +Веб-панель также использует `/api/command` для действий оператора, а не только для запросов на чтение. + +Примеры: + +- `advert` +- `reboot` +- `start ota` +- `time ` +- `time.force ` + +Это мощные команды. Относитесь к ним так же, как к прямому доступу к CLI через последовательный порт. + +Пример: + +```bash +curl -sk https:///api/command \ + -H "X-Auth-Token: $TOKEN" \ + --data 'advert' +``` + +### 5. Помощники удалённой настройки + +Веб-панель сохраняет настройки, генерируя команды CLI и отправляя их через `/api/command`. + +Это значит, что ваши собственные инструменты могут делать то же самое для специфичных для PosadMesh настроек, таких как: + +- поля идентичности репитера +- информация о владельце +- переключатели MQTT-брокера +- метаданные владельца MQTT PosadMesh +- настройки радио, поддерживаемые CLI репитера + +Это практичный способ автоматизировать настройку, сохраняя существующую семантику CLI. + +## Пример скрипта + +Этот пример на shell выполняет вход, получает сводную статистику, получает одну серию тренда и запускает команду CLI: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="https://192.168.1.123" +PASSWORD="your-admin-password" + +TOKEN=$(curl -sk -X POST "$BASE_URL/login" --data "$PASSWORD") + +echo "Summary:" +curl -sk "$BASE_URL/api/stats" \ + -H "X-Auth-Token: $TOKEN" + +echo +echo "Memory trend:" +curl -sk "$BASE_URL/api/stats?series=memory" \ + -H "X-Auth-Token: $TOKEN" + +echo +echo "MQTT status:" +curl -sk "$BASE_URL/api/command" \ + -H "X-Auth-Token: $TOKEN" \ + --data 'get mqtt.status' +``` + +## Случаи ошибок + +Частые ответы: + +- `401 Unauthorized`: токен отсутствует или истёк +- `503 Service Unavailable`: статистика отключена +- `404 No stats data`: запрошенный payload статистики не удалось собрать +- `400 Bad request`: некорректное тело запроса входа или команды + +Если запросы статистики не выполняются: + +1. убедитесь, что веб-панель включена +2. убедитесь, что `web.stats` включён +3. убедитесь, что токен сессии всё ещё действителен +4. снизьте частоту опроса, если плата испытывает нехватку памяти + +## Практические рекомендации + +- предпочитайте `/api/command`, когда нужен точный паритет с CLI +- предпочитайте `/api/stats` для дашбордов и представлений трендов +- держите опрос умеренным, особенно на репитерах с двумя активными MQTT-соединениями +- если вы закончили диагностику, рассмотрите отключение веб-панели командой `set web off`, чтобы максимизировать запас heap на ограниченных платах diff --git a/posadmesh-docs/boards.md b/posadmesh-docs/boards.md new file mode 100644 index 00000000..96669ef9 --- /dev/null +++ b/posadmesh-docs/boards.md @@ -0,0 +1,196 @@ +# Сравнение плат + +Эта страница призвана помочь выбрать плату, а не просто перечислить все технические подробности. + +## С чего начать + +Если вы новичок в железе MeshCore и просто хотите разумную отправную точку, сначала решите, будет устройство companion-радио или репитером. + +Для companion-радио думайте о Wi-Fi-эндпоинте автоматизации: + +- используйте сборки companion Wi-Fi, когда другой системе — например, Home Assistant, RemoteTerm или вашему собственному инструментарию — нужно подключаться к MeshCore по Wi-Fi +- обычно это не устройства для повседневной ручной работы, поэтому экран здесь скорее опция, чем обязательное требование +- платы без экрана или простые платы часто подходят, когда companion-радио будет стоять рядом с питанием и управляться из другого приложения или системы автоматизации +- смотрите таблицы companion-радио ниже, если вам всё же нужен дисплей, GPS или более полный набор на плате + +Для MQTT/веб-репитеров думайте о Wi-Fi, видимости в брокере и достаточном запасе для локальной веб-панели: + +- обычные отправные точки — `Heltec_v3` и `heltec_v4` +- `heltec_v4_repeater_observer` — сильный универсальный выбор обсервера с GPS, PSRAM и 16 MB флеш-памяти +- выбирайте платы с большим объёмом флеш-памяти и PSRAM, если вам нужны MQTT вместе с локальной веб-панелью, статистика или больше запаса под будущие функции +- экраны не являются важным требованием, потому что у обсерверов есть локальная веб-панель для настройки и диагностики +- репитеры без экрана часто оказываются самым чистым решением, когда устройство будет жить на крыше, в корпусе или в стационарной установке, а настраивать вы будете его из веб-панели, последовательной консоли или приложения + +Для репитеров-мостов думайте о локальном радиосвязывании между близко расположенными репитерами: + +- используйте прошивку моста, когда двум локальным репитерам нужно обмениваться трафиком между разными радиоконфигурациями MeshCore, например `Australia (Narrow)` и `Australia (Mid)` +- чистым репитерам-мостам ESP-NOW не нужны ни MQTT, ни веб-панель PosadMesh, ни экран +- ставьте на первое место стабильное питание, подходящие антенны и размещение, а не возможности дисплея +- используйте `observer-posadmesh-bridge-espnow` только когда тому же репитеру нужны ещё и MQTT-аплинк, и локальный мост ESP-NOW; иначе `repeater-bridge-espnow` делает роль проще + +Другие примечания: + +- экраны важнее для companion-радио или стендовых/полевых устройств, чем для стационарных репитеров +- если вам действительно нужен встроенный экран, которым люди будут пользоваться, предпочитайте платы с TFT +- если нужен экран состояния с низким энергопотреблением, предпочитайте платы с электронной бумагой +- если важно определение местоположения или мобильное использование, предпочитайте платы с поддержкой GPS, например семейство T-Beam, `heltec_tracker_v2`, `Heltec_v3`, `heltec_v4` или `ThinkNode_M5` +- если вы хотите самые консервативные варианты на более старом семействе радио, платы на `SX1276` — это `LilyGo_TLora_V2_1_1_6`, `Tbeam_SX1276` и `Heltec_v2` + +## О таблицах + +Таблицы ниже построены на основе метаданных плат PlatformIO из репозитория и флагов сборки вариантов. + +- `Target` — краткое имя платы, используемое на этой странице сравнения. Имена файлов релизов и локальные команды сборки по-прежнему используют полные имена env, например `heltec_v4_repeater_observer`. +- `MCU` показывает только семейство чипа. Фактовая рабочая частота может различаться в зависимости от env и конфигурации платы. +- `RAM` — встроенная память RAM микроконтроллера. +- `PSRAM` — дополнительная память на некоторых платах. Больше PSRAM обычно означает больше запаса для интерфейса, MQTT и будущих функций. +- `GPS` содержит `✅`, когда модуль есть, и остаётся пустым, когда его нет. +- `SD` содержит `✅`, когда для платы на данный момент известно, что она поддерживает путь архива на SD в PosadMesh, `🧪` — когда железо, вероятно, поддерживает TF/microSD, но интеграцию для конкретной платы ещё нужно проверить, и остаётся пустым, когда заметок о поддержке SD/архива сейчас нет. + +## Примечания по трекам + +- Платы `companion-wifi` предназначены для companion-устройств, подключаемых к приложению. +- Платы `observer-posadmesh` предназначены для Wi-Fi-репитеров, публикующих данные в MQTT. +- Платы `repeater-bridge-espnow` и `observer-posadmesh-bridge-espnow` предназначены для использования в качестве локального моста ESP-NOW между близко расположенными репитерами с разными радиоконфигурациями MeshCore. + +Прошивка моста — это не MQTT через WAN и не мост через VPN. Используйте её, когда двум локальным репитерам нужно обмениваться трафиком между радиоконфигурациями, такими как `Australia (Narrow)` и `Australia (Mid)`. + +## Дополнительные признаки + +Если вы выбираете между в остальном похожими платами, эти настройки уровня таргета часто важнее, чем один лишь объём флеш-памяти. + +- `CPU cfg` — рабочая частота, которую MeshCore задаёт для этого таргета. `default` означает, что MeshCore не переопределяет частоту платы по умолчанию. +- `TX cfg` — настроенное в MeshCore значение `LORA_TX_POWER`, а не гарантированная выходная мощность антенны. Платы с PA или RF-фронтендом могут излучать значительно больше, чем предполагает настроенное значение. +- `Power` — то, как таргет представляет питание в репозитории: `ADC batt`, `PMU`, `custom 2S`, `fixed/ext` или `none`. +- `Extras` перечисляет заметные дополнительные возможности RF или периферии, которые предоставляет таргет, например `PA/FEM`, `RF switch`, `BME280`, `NeoPixel` или `ext ant`. + +| Таргет | CPU cfg | TX cfg | Питание | Дополнительно | +| ------------------------ | ------------ | ---------------- | ------------ | ---------------- | +| Ebyte_EoRa-S3 | по умолчанию | 22 дБм конфиг. | АКБ (ADC) | | +| Generic_E22_sx1262 | по умолчанию | 22 дБм конфиг. | АКБ (ADC) | RF-переключатель | +| Generic_E22_sx1268 | по умолчанию | 22 дБм конфиг. | АКБ (ADC) | RF-переключатель | +| Heltec_E213 | по умолчанию | 22 дБм конфиг. | АКБ (ADC) | | +| Heltec_E290 | по умолчанию | 22 дБм конфиг. | АКБ (ADC) | | +| Heltec_T190 | по умолчанию | 22 дБм конфиг. | АКБ (ADC) | | +| heltec_tracker_v2 | 160 МГц | 9 дБм конфиг.\* | АКБ (ADC) | PA/FEM | +| Heltec_v2 | по умолчанию | 20 дБм конфиг. | АКБ (ADC) | | +| Heltec_v3 | 80 МГц | 22 дБм конфиг. | АКБ (ADC) | | +| heltec_v4 | 80 МГц | 10 дБм конфиг.\* | АКБ (ADC) | PA/FEM | +| heltec_v4_tft | 80 МГц | 10 дБм конфиг.\* | АКБ (ADC) | PA/FEM | +| Heltec_Wireless_Paper | по умолчанию | 22 дБм конфиг. | АКБ (ADC) | | +| Heltec_Wireless_Tracker | 80 МГц | 22 дБм конфиг. | нет | | +| Heltec_WSL3 | 80 МГц | 22 дБм конфиг. | АКБ (ADC) | | +| LilyGo_T3S3_sx1262 | default | 22 dBm cfg | ADC batt | | +| LilyGo_T3S3_sx1276 | default | 20 dBm cfg | ADC batt | RF switch | +| LilyGo_TBeam_1W | default | 22 dBm cfg\* | custom 2S | PA/FEM | +| LilyGo_TDeck | default | 22 dBm cfg | ADC batt | | +| LilyGo_Tlora_C6 | default | 22 dBm cfg | none | RF switch | +| LilyGo_TLora_V2_1_1_6 | default | 20 dBm cfg | ADC batt | | +| M5Stack_Unit_C6L | default | 22 dBm cfg | none | RF switch | +| Meshadventurer_sx1262 | default | 22 dBm cfg | ADC batt | RF switch | +| Meshadventurer_sx1268 | default | 22 dBm cfg | ADC batt | RF switch | +| Meshimi | default | 22 dBm cfg | none | ext ant | +| nibble_screen_connect | default | 22 dBm cfg | none | NeoPixel | +| RAK_3112 | 80 MHz | 22 dBm cfg | ADC batt | | +| Station_G2 | default | 7 dBm cfg\* | fixed/ext | PA/FEM | +| Station_G2_logging | default | 7 dBm cfg\* | fixed/ext | PA/FEM | +| T_Beam_S3_Supreme_SX1262 | default | 22 dBm cfg | PMU | BME280 | +| Tbeam_SX1262 | default | 22 dBm cfg | PMU | | +| Tbeam_SX1276 | default | 20 dBm cfg | PMU | | +| ThinkNode_M2 | default | 22 dBm cfg | ADC batt | | +| ThinkNode_M5 | default | 22 dBm cfg | ADC batt | | +| WHY2025_badge | default | 22 dBm cfg | none | | +| Xiao_C6 | default | 22 dBm cfg | none | RF switch | +| Xiao_S3_LoRa2021F33-2G4 | default | 22 dBm cfg | ADC batt | RF switch | +| Xiao_S3_WIO | default | 22 dBm cfg | none | RF switch | + +\* Эти таргеты используют PA или RF-фронтенд, поэтому фактическая выходная мощность может быть заметно выше заданной в `LORA_TX_POWER`. + +## Платы `repeater_observer` + +В таблице перечислены все обсервер-таргеты, определённые сейчас в `variants/posadmesh_mqtt/platformio.ini`. + +| Target | MCU | RAM | PSRAM | Flash | LoRa | Display | GPS | SD | +| ------------------------ | ------- | ------ | ----- | ----- | ------ | --------------- | --- | --- | +| Ebyte_EoRa-S3 | ESP32S3 | 512 KB | 2 MB | 4 MB | SX1262 | OLED (SSD1306) | | | +| Generic_E22_sx1262 | ESP32 | 520 KB | No | 4 MB | SX1262 | None | | | +| Generic_E22_sx1268 | ESP32 | 520 KB | No | 4 MB | SX1268 | None | | | +| Heltec_E213 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | E-paper (2.13") | | | +| Heltec_E290 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | E-paper (2.9") | | | +| Heltec_T190 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | TFT (ST7789) | | | +| heltec_tracker_v2 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | TFT (ST7735) | ✅ | | +| Heltec_v2 | ESP32 | 520 KB | No | 8 MB | SX1276 | OLED (SSD1306) | | | +| Heltec_v3 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | OLED (SSD1306) | ✅ | | +| heltec_v4 | ESP32S3 | 512 KB | 2 MB | 16 MB | SX1262 | OLED (SSD1306) | ✅ | | +| heltec_v4_tft | ESP32S3 | 512 KB | 2 MB | 16 MB | SX1262 | TFT (ST7789) | ✅ | | +| Heltec_Wireless_Paper | ESP32S3 | 512 KB | No | 8 MB | SX1262 | E-paper (2.13") | | | +| Heltec_Wireless_Tracker | ESP32S3 | 512 KB | No | 8 MB | SX1262 | TFT (ST7735) | ✅ | | +| Heltec_WSL3 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | None | ✅ | | +| LilyGo_T3S3_sx1262 | ESP32S3 | 512 KB | 2 MB | 4 MB | SX1262 | OLED (SSD1306) | | 🧪 | +| LilyGo_T3S3_sx1276 | ESP32S3 | 512 KB | 2 MB | 4 MB | SX1276 | OLED (SSD1306) | | 🧪 | +| LilyGo_TBeam_1W | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | OLED (SH1106) | ✅ | ✅ | +| LilyGo_TDeck | ESP32S3 | 512 KB | No | 16 MB | SX1262 | TFT (ST7789) | ✅ | 🧪 | +| LilyGo_Tlora_C6 | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | | | +| M5Stack_Unit_C6L | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | ✅ | | +| Meshadventurer_sx1262 | ESP32 | 520 KB | No | 4 MB | SX1262 | OLED (SSD1306) | ✅ | | +| Meshadventurer_sx1268 | ESP32 | 520 KB | No | 4 MB | SX1268 | OLED (SSD1306) | ✅ | | +| Meshimi | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | | | +| nibble_screen_connect | ESP32S3 | 512 KB | No | 4 MB | SX1262 | OLED (SSD1306) | | | +| RAK_3112 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | None | ✅ | | +| Station_G2 | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | OLED (SH1106) | ✅ | | +| Station_G2_logging | ESP32S3 | 512 KB | 8 MB | 16 MB | SX1262 | OLED (SH1106) | ✅ | | +| T_Beam_S3_Supreme_SX1262 | ESP32S3 | 512 KB | 8 MB | 8 MB | SX1262 | OLED (SH1106) | ✅ | ✅ | +| Tbeam_SX1262 | ESP32 | 520 KB | No | 4 MB | SX1262 | OLED (SSD1306) | ✅ | | +| Tbeam_SX1276 | ESP32 | 520 KB | No | 4 MB | SX1276 | OLED (SSD1306) | ✅ | | +| ThinkNode_M2 | ESP32S3 | 512 KB | No | 4 MB | SX1262 | OLED (SH1106) | | | +| ThinkNode_M5 | ESP32S3 | 512 KB | No | 4 MB | SX1262 | E-paper (GxEPD) | ✅ | | +| WHY2025_badge | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | | | +| Xiao_C6 | ESP32C6 | 512 KB | No | 4 MB | SX1262 | None | | | +| Xiao_S3_LoRa2021F33-2G4 | ESP32S3 | 512 KB | No | 8 MB | LR2021 | None | | | +| Xiao_S3_WIO | ESP32S3 | 512 KB | No | 8 MB | SX1262 | None | ✅ | | + +Замечания по SD: + +- `✅` — плата сейчас проверенно работает с архивом статистики на SD в PosadMesh. +- `🧪` — аппаратно плата, скорее всего, поддерживает TF/microSD, но интеграцию для конкретной платы в PosadMesh ещё нужно проверить, прежде чем считать её поддерживаемой. +- Для текущего архива статистики карты microSD `4 GB` `FAT32` более чем достаточно. Карты большего объёма тоже поддерживаются. + +## Платы `companion_radio_wifi` с дисплеем + +Это Wi-Fi companion-таргеты, у которых настроен дисплей для локального статуса, подсказок при настройке или эпизодического прямого взаимодействия. + +| Target | MCU | RAM | PSRAM | Flash | LoRa | Display | GPS | +| ------------------------ | ------- | ------ | ----- | ----- | ------ | --------------- | --- | +| Heltec_v2 | ESP32 | 520 KB | No | 8 MB | SX1276 | OLED (SSD1306) | | +| Heltec_v3 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | OLED (SSD1306) | ✅ | +| heltec_tracker_v2 | ESP32S3 | 512 KB | No | 8MB | SX1262 | TFT (ST7735) | ✅ | +| heltec_v4 | ESP32S3 | 512 KB | 2 MB | 16MB | SX1262 | OLED (SSD1306) | ✅ | +| heltec_v4_tft | ESP32S3 | 512 KB | 2 MB | 16MB | SX1262 | TFT (ST7789) | ✅ | +| LilyGo_TBeam_1W | ESP32S3 | 512 KB | 8 MB | 16MB | SX1262 | OLED (SH1106) | ✅ | +| LilyGo_TLora_V2_1_1_6 | ESP32 | 520 KB | No | 4 MB | SX1276 | OLED (SSD1306) | ✅ | +| nibble_screen_connect | ESP32S3 | 512 KB | No | 4MB | SX1262 | OLED (SSD1306) | | +| Station_G2 | ESP32S3 | 512 KB | 8 MB | 16MB | SX1262 | OLED (SH1106) | ✅ | +| T_Beam_S3_Supreme_SX1262 | ESP32S3 | 512 KB | 8 MB | 8MB | SX1262 | OLED (SH1106) | ✅ | +| ThinkNode_M2 | ESP32S3 | 512 KB | No | 4MB | SX1262 | OLED (SH1106) | | +| ThinkNode_M5 | ESP32S3 | 512 KB | No | 4MB | SX1262 | E-paper (GxEPD) | ✅ | + +## Платы `companion_radio_wifi` без дисплея + +Это Wi-Fi companion-таргеты, где основной интерфейс — приложение companion. Для многих пользователей это лучший повседневный вариант. + +| Target | MCU | RAM | PSRAM | Flash | LoRa | GPS | +| ----------------------- | ------- | ------ | ----- | ----- | ------ | --- | +| Heltec_WSL3 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | ✅ | +| RAK_3112 | ESP32S3 | 512 KB | No | 8 MB | SX1262 | ✅ | +| Xiao_S3_LoRa2021F33-2G4 | ESP32S3 | 512 KB | No | 8 MB | LR2021 | | +| Xiao_S3_WIO | ESP32S3 | 512 KB | No | 8 MB | SX1262 | ✅ | + +## Практические рекомендации + +- Лучший универсальный обсервер с экраном и хорошим запасом ресурсов: `heltec_v4_repeater_observer`, `heltec_v4_tft_repeater_observer`, `Station_G2_repeater_observer`, `LilyGo_TBeam_1W_repeater_observer`. +- Лучший обсервер для простой установки без дисплея: `RAK_3112_repeater_observer`, `Generic_E22_sx1262_repeater_observer`, `Meshimi_repeater_observer`, `Xiao_C6_repeater_observer`. +- Лучшие малопотребляющие сборки обсервера с дисплеем: `Heltec_E213_repeater_observer`, `Heltec_E290_repeater_observer`, `Heltec_Wireless_Paper_repeater_observer`, `ThinkNode_M5_Repeater_observer`. +- Лучший выбор companion, если нужен самый богатый локальный интерфейс: `heltec_v4_tft_companion_radio_wifi` и `heltec_tracker_v2_companion_radio_wifi`. +- Лучший выбор companion, если нужен максимальный запас памяти: `T_Beam_S3_Supreme_SX1262_companion_radio_wifi`, `Station_G2_companion_radio_wifi`, `LilyGo_TBeam_1W_companion_radio_wifi`. +- Лучший выбор companion, если достаточно приложения и работы почти без дисплея: `RAK_3112_companion_radio_wifi`, `Xiao_S3_WIO_companion_radio_wifi`, `Station_G2_companion_radio_wifi`. +- Лучший выбор companion, если вы предпочитаете OLED вместо TFT: `T_Beam_S3_Supreme_SX1262_companion_radio_wifi`, `heltec_v4_companion_radio_wifi`, `LilyGo_TBeam_1W_companion_radio_wifi`. diff --git a/posadmesh-docs/custom-cli.md b/posadmesh-docs/custom-cli.md new file mode 100644 index 00000000..6a117f18 --- /dev/null +++ b/posadmesh-docs/custom-cli.md @@ -0,0 +1,313 @@ +# Собственные команды CLI + +Эта страница описывает команды CLI, специфичные для PosadMesh, добавленные в этом репозитории. + +Она не пытается повторять весь набор команд CLI апстримного MeshCore. + +## С чего начать + +Если вы выполняете первичную настройку, вот команды, которые нужны большинству пользователей прежде всего: + +```text +set wifi.ssid +set wifi.pwd +get wifi.status +set mqtt.iata +get mqtt.status +``` + +Для обсерверов с локальной веб-панелью также полезны: + +```text +set web on +get web.status +set web off +``` + +Используйте `set web on` при настройке или диагностике, а затем `set web off`, когда стационарному обсерверу нужен максимальный запас памяти. + +## Команды обсервера + +Эти команды доступны в таргетах прошивки `*_repeater_observer`. + +Команды `get` без аргументов нужно вводить точно так, как показано. + +### Статус и маршрутизация MQTT + +- `get mqtt.status`: показывает состояние Wi-Fi, NTP, IATA, эндпоинта, транспорт пользовательского эндпоинта, состояние публикации статуса, состояние TX и `leaked:` (MQTT-клиенты, намеренно оставленные неосвобождёнными после сбоя целостности heap при завершении; должно оставаться `0`). +- `get mqtt.statuscfg`: показывает, включены ли периодические сообщения статуса, в виде простого значения `on` или `off`. Большинству пользователей достаточно `get mqtt.status`. +- `get mqtt.client_version`: показывает строку MQTT `client_version`, публикуемую репитером. +- `get mqtt.client_env`: показывает окружение PlatformIO, использованное для сборки прошивки репитера. +- `get mqtt.iata`: показывает код IATA/местоположения, используемый в топиках MQTT. +- `set mqtt.iata `: задаёт код IATA/местоположения, например `MEL`. +- `set mqtt.iata UNSET`: помечает MQTT IATA как ещё не настроенный. Пока он `UNSET`, включённые MQTT-брокеры остаются отключёнными, пока не будет сохранён реальный код. + +### Идентификация MQTT + +- `get mqtt.owner`: показывает настроенный публичный ключ владельца. +- `set mqtt.owner <64-hex-char-public-key>`: задаёт публичный ключ владельца, используемый в метаданных JWT. +- `mqtt.owner <64-hex-char-public-key>`: сокращённая форма для задания публичного ключа владельца. +- `get mqtt.email`: показывает настроенный email владельца. +- `set mqtt.email `: задаёт email владельца, используемый в метаданных JWT. +- `mqtt.email `: сокращённая форма для задания email владельца. + +### Управление сообщениями MQTT + +- `get mqtt.packets`: показывает, публикуются ли сообщения с пакетами. +- `set mqtt.packets on|off`: включает или отключает публикацию пакетов. +- `get mqtt.raw`: показывает, публикуются ли необработанные payload пакетов. +- `set mqtt.raw on|off`: включает или отключает отдельный топик MQTT `raw`. +- `set mqtt.status on|off`: включает или отключает периодическую публикацию статуса MQTT. +- `get mqtt.tx`: показывает, включаются ли пакеты TX. +- `set mqtt.tx on|off`: включает или отключает публикацию пакетов TX. + +### Эндпоинты MQTT + +- `get mqtt.meshcoretel` +- `set mqtt.meshcoretel on|off` +- `get mqtt.custom` +- `set mqtt.custom on|off` +- `get mqtt.custom.host` +- `set mqtt.custom.host ` +- `get mqtt.custom.port` +- `set mqtt.custom.port ` +- `get mqtt.custom.transport`: показывает `tcp` или `wss`. +- `set mqtt.custom.transport tcp|wss` +- `get mqtt.custom.username` +- `set mqtt.custom.username ` +- `get mqtt.custom.password`: показывает `set`, когда пользовательский пароль настроен. +- `set mqtt.custom.password ` + +Примечания: + +- новые установки обсерверов по умолчанию задают для `mqtt.iata` значение `UNSET` +- одновременно можно включить максимум два MQTT-брокера +- если `mqtt.iata` имеет значение `UNSET`, включённые MQTT-брокеры не подключатся +- пользовательский MQTT использует настроенные имя пользователя и пароль, а не аутентификацию JWT +- пользовательский MQTT по умолчанию использует TCP; задайте `mqtt.custom.transport wss` для MQTT через защищённые WebSockets с фиксированным путём websocket `/mqtt` и корневым набором сертификатов CA x509 из ESP-IDF +- `get mqtt.status` выводит `> wifi: ntp: iata: meshcoretel: custom:: status: tx:`; состояния брокера: `up` (подключён), `conn` (подключение), `wait` (нет Wi-Fi или синхронизации времени), `backoff`, `retry`, `unconfigured`, `invalid iata` или `off` +- пользовательский MQTT использует те же топики `meshcore///`, что и курируемые брокеры +- выключение подключённого брокера публикует retained-обновление статуса MQTT с `"status":"offline"` перед отключением клиента +- изменение `mqtt.iata` относительно настроенного значения также публикует retained offline-статус в старый топик статуса, перезапускает подключённые клиенты брокеров и переподключается по новому пути топика + +Также принимаются устаревшие алиасы с точками: + +- `mqtt.meshcoretel.ru` + +### Настройки Wi-Fi для обсерверов + +- `get wifi.status`: показывает SSID, состояние подключения, необработанный код статуса Wi-Fi, IP, канал и уровень сигнала при подключении, а также состояние шлюза (`gw:ok|lost`) и счётчик переподключений сторожевым таймером (`wd:`). +- `get wifi.ssid`: показывает настроенный SSID Wi-Fi. +- `set wifi.ssid `: задаёт SSID Wi-Fi. +- `set wifi.pwd `: задаёт пароль Wi-Fi. +- `get wifi.powersaving`: показывает текущий режим энергосбережения Wi-Fi. +- `set wifi.powersaving none|min|max`: задаёт режим энергосбережения Wi-Fi. +- `wifi reconnect`: разрывает текущее соединение и переподключается с полным сканированием каналов. Используйте, когда узел сообщает о подключении, но недоступен по сети. + +Обсерверы также используют сторожевой таймер связи: пока Wi-Fi сообщает о подключении, узел каждые 30 секунд проверяет свой шлюз через ARP. Если шлюз молчит 3 минуты (например, точка доступа, которая продолжает рассылать beacon после потери проводного аплинка), узел сам принудительно выполняет полное переподключение, увеличивая интервал между попытками вплоть до 48 минут, пока сбой не прекратится. `wd:` в `get wifi.status` считает такие принудительные переподключения с момента загрузки. + +### Настройки NTP для обсерверов + +- `get ntp.server1`: показывает основной NTP-сервер. +- `get ntp.server2`: показывает дополнительный NTP-сервер. +- `get ntp.server3`: показывает третий NTP-сервер. +- `set ntp.server1 `: задаёт основной NTP-сервер и перезапускает синхронизацию времени. +- `set ntp.server2 `: задаёт дополнительный NTP-сервер и перезапускает синхронизацию времени. +- `set ntp.server3 `: задаёт третий NTP-сервер и перезапускает синхронизацию времени. + +Серверы по умолчанию: `au.pool.ntp.org`, `time.google.com` и `time.cloudflare.com`. + +### Настройки моста ESP-NOW для сборок Observer ESP-NOW + +Эти команды доступны в таргетах прошивки `*_repeater_observer_espnow`, использующих транспорт моста ESP-NOW. + +Команды моста предназначены для локального использования моста ESP-NOW между близко расположенными репитерами, например для связи репитеров в `Australia (Narrow)` и `Australia (Mid)`. Это не элементы управления MQTT-over-WAN, VPN или интернет-мостом. + +- `get bridge.channel`: показывает настроенный канал моста ESP-NOW. +- `set bridge.channel `: задаёт канал моста ESP-NOW и перезапускает мост. Используйте значение от `1` до `14`. +- `get bridge.secret`: показывает настроенный секрет моста ESP-NOW. +- `set bridge.secret `: задаёт общий секрет моста ESP-NOW и перезапускает мост. + +После выполнения `set bridge.channel` ожидайте кратковременного разрыва соединения моста и веб-панели, пока радио перезапускается. В текущих сборках observer ESP-NOW это может выглядеть как перезагрузка платы. + +В сборках `*_repeater_observer_espnow`, подключённых к Wi-Fi, канал моста ESP-NOW должен совпадать с активным каналом Wi-Fi 2,4 ГГц: + +1. Выполните `get wifi.status`. +2. Считайте значение `channel:` из статуса подключённого Wi-Fi. +3. Выполните `get bridge.channel`. +4. Если значения различаются, выполните `set bridge.channel `, подставив значение канала Wi-Fi. +5. Используйте одинаковые `bridge.channel` и `bridge.secret` на каждом узле моста ESP-NOW, которые должны взаимодействовать друг с другом. + +Пример: + +```text +> get wifi.status +> ssid:EastMesh-IoT status:connected code:3 state:connected ip:192.168.1.50 channel:6 rssi:-61 quality:78% signal:good gw:ok wd:0 +> get bridge.channel +> 1 +> set bridge.channel 6 +OK +``` + +### Элементы управления веб-панели + +- `get web` +- `get web.status`: показывает, доступна ли локальная HTTPS-панель, а также `heals:` (сколько раз панель перезапускалась из-за слишком сильной фрагментации внутреннего heap для TLS-рукопожатия) и `deferred:` (попытки запуска, отложенные до восстановления запаса heap). +- `get web.stats.status`: показывает, включены ли выделенная страница статистики и подсистема истории, активна ли последняя история, доступна ли история в PSRAM и смонтирован ли архив на SD. Когда это включено, сбор истории теперь охватывает и поддерживаемую телеметрию окружающей среды, а не только исходные серии по батарее и радио. Платы с активным GPS также записывают поминутные выборки по спутникам для исторического вида `/stats`. Если доступ к архиву пропадает, пока статистика остаётся включённой, репитер периодически повторяет монтирование SD. +- При загрузке восстановление статистики из архива использует ограниченное чтение последней сводки, событий и снимков соседей, чтобы повреждённые или неожиданно большие файлы архива не задерживали запуск MQTT или веб-панели. +- `set web on|off` +- `set.web on|off`: включает или отключает локальную HTTPS-панель. +- `set web.stats on|off` +- `set.web.stats on|off`: включает или отключает выделенную страницу `/stats` и сбор исторической статистики. +- `purge sd`: удаляет файлы и каталоги из смонтированного архива на SD-карте, затем заново создаёт пустой каталог архива `/stats`, чтобы сбор статистики во время работы мог продолжаться. Это не стирает внутреннюю файловую систему репитера или сохранённые настройки. + +### Диагностика во время работы + +- `memory`: показывает текущее использование heap и PSRAM. +- `stats-core`: показывает батарею, время работы, счётчик неустранимых ошибок и глубину очереди исходящих. +- `stats-radio`: показывает уровень шума радио, последний RSSI, последний SNR и время в эфире TX/RX. +- `stats-packets`: показывает суммарные значения приёма/передачи пакетов, разбивку flood/direct и ошибки приёма. + +> Если `noise_floor` показывает `0`, проверьте `get agc.reset.interval`; если значение не `0`, попробуйте `set agc.reset.interval 0` и проверьте снова. + +### Лимит пересылки flood + +- `get flood.max.unscoped`: показывает лимит хопов для неограниченных flood-пакетов. +- `set flood.max.unscoped <0-64>`: задаёт лимит хопов для неограниченных flood-пакетов. +- `get flood.max.advert`: показывает лимит хопов для flood-пакетов advert. +- `set flood.max.advert <0-64>`: задаёт лимит хопов для flood-пакетов advert. + +В сборках обсерверов `flood.max.unscoped` по умолчанию равен `64`. Меньшие значения могут ограничить, как далеко повторяется неограниченный flood-трафик, тогда как пересылка scoped/region flood остаётся под управлением `flood.max`. + +### Отчётность о батарее платы + +- В сборках обсерверов фоновый опрос батареи, используемый для истории MQTT/статуса, ограничен примерно одним разом в минуту. Явные запросы статуса и телеметрии по-прежнему обновляют показание сразу. + +### Управление вентилятором T-Beam 1W + +Эти команды доступны только в сборках репитера `LilyGo_TBeam_1W_*`. + +- `get fan`: показывает текущий режим вентилятора, текущее состояние вентилятора и последнюю температуру платы по NTC, когда она доступна. +- `set fan auto`: возвращает вентилятор к автоматическому управлению и сохраняет этот режим после перезагрузки. +- `set fan on`: принудительно включает вентилятор и сохраняет этот режим после перезагрузки. +- `set fan off`: принудительно выключает вентилятор и сохраняет этот режим после перезагрузки. +- `set fan timeout `: изменяет автоматическое окно удержания после TX в секундах и сохраняет его после перезагрузки, например `set fan timeout 45s`. + +Поведение автоматического режима: + +- принудительно включает вентилятор во время TX и оставляет его включённым на настроенный таймаут после этого +- в остальных случаях включает вентилятор при `48C` +- выключает его обратно при `42C` +- оставляет вентилятор включённым, если показание NTC недоступно + +Примечания: + +- режим вентилятора репитера по умолчанию — `auto` +- таймаут после TX по умолчанию — `30s` +- режим вентилятора и таймаут хранятся в prefs репитера и сохраняются после перезагрузки +- только сборки репитера `LilyGo_TBeam_1W_*` используют эти сохраняемые настройки вентилятора +- допустимый диапазон — от `0s` до `600s` + +## Доступ к CLI через веб-панель + +Когда веб-панель репитера включена и вы прошли аутентификацию, панель CLI в браузере может выполнять те же команды CLI, которые принимает репитер. + +Примечания: + +- панель по-прежнему использует для доступа пароль администратора репитера +- команды выполняются с той же осторожностью, как если бы вы вводили их напрямую в CLI репитера +- это предназначено для локального административного использования в доверенной сети +- `start ota` освобождает локальный слушатель HTTP-редиректа на порту `80`, чтобы слушатель OTA HTTP мог занять его, не останавливая остальные службы репитера, независимо от того, выполнена ли команда из веб-панели, последовательного CLI или удалённого сеанса CLI companion/приложения +- если старый слушатель редиректа не освободил порт `80` полностью, слушатель OTA повторяет попытки примерно до 30 секунд, после чего сдаётся +- кнопка `Purge SD` в веб-интерфейсе выполняет `purge sd` после подтверждения в браузере +- `start ota` использует существующий Wi-Fi-адрес репитера, если он уже подключён, или запускает точку доступа `MeshCore-OTA`, если Wi-Fi не подключён +- ярлык Regions в `/app` последовательно выполняет существующие команды регионов MeshCore: `region put au`, `region put au-STATE`, `region allowf au`, `region allowf au-STATE`, затем `region save` + +## Команды восстановления Wi-Fi для companion + +Эти команды доступны в последовательном CLI восстановления для сборок `*_companion_radio_wifi`. + +Чтобы войти в `CLI Rescue`: + +- откройте последовательный монитор на скорости `115200` бод +- перезагрузите устройство +- удерживайте кнопку пользователя в течение первых 8 секунд после загрузки +- дождитесь `========= CLI Rescue =========` + +- `get wifi.status`: показывает настроенный SSID, состояние подключения, необработанный код статуса Wi-Fi, IP, канал и уровень сигнала при подключении. +- `get wifi.ssid`: показывает настроенный SSID Wi-Fi. +- `get wifi.powersaving`: показывает текущий режим энергосбережения Wi-Fi. +- `set wifi.ssid `: сохраняет SSID Wi-Fi и сразу повторяет подключение. +- `set wifi.pwd `: сохраняет пароль Wi-Fi и сразу повторяет подключение. +- `set wifi.powersaving none|min|max`: изменяет режим энергосбережения Wi-Fi. + +Сборки Companion Wi-Fi также по-прежнему поддерживают существующие команды восстановления, такие как: + +- `set pin <6-digit-pin>` +- `rebuild` +- `erase` +- `ls ...` +- `cat ...` +- `rm ...` +- `reboot` + +### Точка доступа для восстановления + +Когда у устройства companion Wi-Fi не настроены учётные данные Wi-Fi или оно не может подключиться +к своей настроенной сети в течение 60 секунд, оно поднимает собственную +точку доступа `EastMesh-WiFi`, чтобы его можно было восстановить без последовательного кабеля. + +**Пароль точки доступа — это pin устройства, дополненный нулями до 8 цифр** (WPA2 требует +не менее 8 символов). Pin подчиняется тем же правилам, что и pin сопряжения Bluetooth в +сборках BLE: + +| Устройство | Активный pin | Пароль `EastMesh-WiFi` | +| --------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------- | +| Есть экран, pin не задан | случайный 6-значный pin при каждой загрузке, отображается как `Pin:NNNNNN` на главном экране | `00NNNNNN` — считайте его с экрана | +| Нет экрана, pin не задан | `123456` (по умолчанию) | `00123456` | +| Pin задан через `set pin ` | ваш настроенный pin (любое устройство) | ваш pin, дополненный нулями до 8 цифр, например pin `4242` → `00004242` | + +Примечания по pin: + +- на устройствах с экраном pin случаен только до тех пор, пока pin не сохранён; выполните + `set pin ` для фиксированного пароля (вступает в силу при следующей загрузке) +- устройствам без экрана в общедоступных местах всегда следует задавать собственный pin — `00123456` — + это документированное значение по умолчанию, поэтому относитесь к нему как к паролю роутера по умолчанию + +Шаги восстановления: + +1. подключитесь к сети `EastMesh-WiFi` с паролем из таблицы выше +2. откройте CLI восстановления командой `telnet 192.168.4.1` (или `nc 192.168.4.1 23`) — все + команды восстановления, перечисленные выше, доступны +3. `set wifi.ssid `, затем `set wifi.pwd ` — устройство сразу + повторяет подключение к сети с новыми учётными данными +4. `reboot` (или просто подождите — см. ниже) + +Пока точка доступа поднята, устройство продолжает повторять попытки подключения к настроенной сети примерно раз в +минуту (ожидайте короткий сбой точки доступа при каждой попытке); как только подключение станции +успешно, точка доступа восстановления автоматически отключается (это также разрывает +ваш сеанс восстановления — это признак того, что всё сработало). + +### Использование приложения companion через AP (роуминг) + +Точка доступа восстановления нужна не только для исправления учётных данных — через неё +доступен полный протокол companion, что делает её режимом роумингового доступа, когда устройство +находится вне своей настроенной сети: + +1. подключитесь к `EastMesh-WiFi` с паролем, производным от pin (см. таблицу выше) +2. в приложении companion MeshCore добавьте/подключите Wi-Fi-устройство с хостом + `192.168.4.1` и портом `5000` +3. пользуйтесь приложением как обычно — сообщения, контакты и каналы работают через AP + +Примечания по использованию в роуминге: + +- один раз выполните `set pin `, чтобы пароль точки доступа оставался неизменным между перезагрузками; + иначе устройства с экраном выбирают новый случайный pin при каждой загрузке +- точка доступа появляется примерно через 60 секунд после загрузки (сначала устройство пытается + подключиться к своей настроенной сети) и работает до тех пор, пока эта сеть недоступна +- оказавшись снова в зоне действия своей настроенной сети, устройство подключается к ней и + автоматически отключает точку доступа — вместо этого переподключите приложение по адресу в локальной сети + +## Скрипт проверки состояния heap + +`posadmesh-tools/web-heap-check.sh [--stress] [--rounds N]` читает `memory`, `get web.status`, `get mqtt.status` и `get wifi.status` через HTTPS API, а с флагом `--stress` имитирует всплески загрузки страниц браузером перед повторным чтением `memory`. Аутентифицируйтесь с помощью `REPEATER_PASSWORD` или `REPEATER_TOKEN` в окружении (или введите пароль по запросу). Следите за `heap_max` (наибольший внутренний блок; для TLS нужно ~40KB), `heap_min` и счётчиками `heals`/`deferred`/`leaked`. diff --git a/posadmesh-docs/index.md b/posadmesh-docs/index.md new file mode 100644 index 00000000..676014f0 --- /dev/null +++ b/posadmesh-docs/index.md @@ -0,0 +1,72 @@ +# Документация MeshCore PosadMesh + +MeshCore-Posadmesh сохраняет upstream-прошивку MeshCore без изменений и публикует четыре трека прошивок в зависимости от того, как устройству нужно подключаться: + +- `companion-wifi`: используйте для companion-устройств с подключением по Wi-Fi. Этот трек наиболее близок к upstream MeshCore и добавляет команды аварийного восстановления/настройки Wi-Fi от PosadMesh. +- `repeater-bridge-espnow`: используйте, когда нужен обычный репитерный мост ESP-NOW в стиле upstream, без MQTT-аплинка и без веб-панели PosadMesh. +- `observer-posadmesh`: используйте для Wi-Fi репитера, который должен публиковать данные в MQTT-брокер и, на поддерживаемых платах ESP32, предоставлять опциональную локальную веб-панель для настройки и диагностики. +- `observer-posadmesh-bridge-espnow`: используйте, когда одному репитеру нужны одновременно MQTT-аплинк и обязанности моста ESP-NOW, включая управление каналом/секретом моста для синхронизации моста с Wi-Fi. + +!!! note "Треки-мосты — это локальные радиомосты" + + Треки-мосты ESP-NOW предназначены для соединения двух расположенных рядом репитеров, работающих на разных радиоконфигурациях MeshCore, например один репитер на `Australia (Narrow)`, а другой на `Australia (Mid)`. + + Треки-мосты — это не релизы MQTT-over-WAN, VPN или интернет-туннелей. + +Если сначала нужны рекомендации, начните с: + +- [Сравнение плат](./boards.md) +- [Скачивание и прошивка релизов](./releases.md) + +Если вы уже знаете свою плату и хотите самый быстрый путь, пропустите документацию и откройте прошивальщик: + +- [Открыть прошивальщик PosadMesh](https://flasher.eastmesh.au) + +## Я хочу + +- выбрать плату: начните с [Сравнение плат](./boards.md) +- прошить прошивку с рекомендациями: начните с [Скачивание и прошивка релизов](./releases.md) +- прошить прошивку сейчас: откройте [прошивальщик PosadMesh](https://flasher.eastmesh.au) +- настроить репитер после прошивки: используйте [Скачивание и прошивка релизов](./releases.md) и [Использование веб-панели репитера](./web-panel.md) +- разобраться в CLI-командах PosadMesh: используйте [Пользовательские CLI-команды](./custom-cli.md) +- автоматизировать работу с репитером или писать скрипты: используйте [Использование веб-API репитера](./api.md) +- собирать прошивку локально: используйте [Локальная сборка с uv](./local-builds.md) + +## Руководства для пользователей + +- [Сравнение плат](./boards.md) +- [Скачивание и прошивка релизов](./releases.md) +- [Миграция с xJARiD/MeshCore](./migration.md) +- [Использование веб-панели репитера](./web-panel.md) +- [Использование веб-API репитера](./api.md) +- [Пользовательские CLI-команды](./custom-cli.md) + +## Заметки для разработчиков + +- [Локальная сборка с uv](./local-builds.md) + +## Текущий охват + +Этот сайт документации охватывает только части, специфичные для PosadMesh, в данном репозитории. + +По общему поведению MeshCore, работе с радио и концепциям upstream-прошивки обращайтесь к upstream-проекту: + +- [meshcore-dev/MeshCore](https://github.com/meshcore-dev/MeshCore) + +## Региональные материалы по Австралии + +Эти материалы поддерживаются сообществом, а не являются официальной документацией проекта, но они всё равно полезны для региональных заметок по настройке в Австралии, местных соглашений и материалов по MeshCore для отдельных регионов. + +### ACT / NSW / QLD / SA / TAS / VIC + +- [wiki.eastmesh.au](https://wiki.eastmesh.au/) +- [wiki.meshcoreaus.org](https://wiki.meshcoreaus.org/) + +### Сидней + +- [nswmesh.au](https://nswmesh.au/) +- [github.com/nswmesh](https://github.com/nswmesh/) + +### Брисбен + +- [wiki.mbug.com.au/en/Meshcore/Settings](https://wiki.mbug.com.au/en/Meshcore/Settings) diff --git a/posadmesh-docs/local-builds.md b/posadmesh-docs/local-builds.md new file mode 100644 index 00000000..0a3e65fe --- /dev/null +++ b/posadmesh-docs/local-builds.md @@ -0,0 +1,200 @@ +# Локальная сборка с помощью uv + +В этом репозитории `uv` используется для инструментов Python, а PlatformIO запускается через `uv run`. + +Эта страница посвящена сборке из исходников. Если вам нужна только готовая прошивка, чтобы прошить устройство, начните со страницы [Скачивание и прошивка релизов](./releases.md). + +Используйте эту страницу, когда изменяете прошивку, тестируете таргет перед релизом или собираете локальный артефакт, недоступный в разделе Releases. + +## Настройка + +Из корня репозитория: + +```bash +uv sync +``` + +## Полезные команды + +Список таргетов сборки: + +```bash +bash posadmesh-build.sh list +``` + +Обычная сборка PlatformIO для одного таргета: + +```bash +uv run pio run -e heltec_v4_repeater_observer +uv run pio run -e heltec_v4_companion_radio_wifi +uv run pio run -e heltec_v4_repeater_bridge_espnow +uv run pio run -e heltec_v4_repeater_observer_espnow +uv run pio run -e Xiao_S3_WIO_repeater_observer_mqtt_bridge +``` + +Прошивка таргета: + +```bash +uv run pio run -e heltec_v4_repeater_observer -t upload --upload-port /dev/tty.usbmodemXXXX +``` + +Монитор последовательного порта: + +```bash +uv run pio device monitor --port /dev/tty.usbmodemXXXX --baud 115200 +``` + +## Локальные сборки в стиле релиза + +Если вам нужны те же метаданные версии, которые используются при выпуске релиза, сначала экспортируйте переменные версии. + +Companion Wi-Fi: + +```bash +export FIRMWARE_VERSION=v1.14.1 +bash posadmesh-build.sh build-firmware heltec_v4_companion_radio_wifi +``` + +Обсервер: + +```bash +export FIRMWARE_VERSION=v1.15.0 +export POSADMESH_VERSION=v2026.5.1 +bash posadmesh-build.sh build-firmware heltec_v4_repeater_observer +``` + +Мост ESP-NOW репитера: + +```bash +export FIRMWARE_VERSION=v1.15.0 +bash posadmesh-build.sh build-firmware heltec_v4_repeater_bridge_espnow +``` + +Обсервер ESP-NOW: + +```bash +export FIRMWARE_VERSION=v1.15.0 +export POSADMESH_VERSION=v2026.5.1 +bash posadmesh-build.sh build-firmware heltec_v4_repeater_observer_espnow +``` + +На выходе получаются артефакты с версиями в `out/`. + +Правило версионирования: + +- `companion-wifi` и `repeater-bridge-espnow` используют версию upstream MeshCore как `FIRMWARE_VERSION` +- `observer-posadmesh`, `observer-posadmesh-bridge-espnow` и `observer-posadmesh-bridge-mqtt` используют версию upstream MeshCore как `FIRMWARE_VERSION` плюс версию релиза PosadMesh как `POSADMESH_VERSION` + +## Поддерживаемые платы `repeater_observer` + +Это полные имена окружений PlatformIO, используемые для локальных сборок из исходников и для именования артефактов релизов. + +```text +Generic_E22_sx1262_repeater_observer +Generic_E22_sx1268_repeater_observer +Heltec_E213_repeater_observer +Heltec_E290_repeater_observer +Heltec_T190_repeater_observer +heltec_tracker_v2_repeater_observer +Heltec_v2_repeater_observer +Heltec_v3_repeater_observer +heltec_v4_repeater_observer +heltec_v4_tft_repeater_observer +Heltec_Wireless_Paper_repeater_observer +Heltec_Wireless_Tracker_repeater_observer +Heltec_WSL3_repeater_observer +LilyGo_T3S3_sx1262_repeater_observer +LilyGo_T3S3_sx1276_repeater_observer +LilyGo_TBeam_1W_repeater_observer +LilyGo_TDeck_repeater_observer +LilyGo_Tlora_C6_repeater_observer +M5Stack_Unit_C6L_repeater_observer +Meshadventurer_sx1262_repeater_observer +Meshadventurer_sx1268_repeater_observer +Meshimi_repeater_observer +nibble_screen_connect_repeater_observer +RAK_3112_repeater_observer +Station_G2_logging_repeater_observer +Station_G2_repeater_observer +T_Beam_S3_Supreme_SX1262_repeater_observer +Tbeam_SX1262_repeater_observer +Tbeam_SX1276_repeater_observer +WHY2025_badge_repeater_observer +Xiao_C6_repeater_observer +Xiao_S3_LoRa2021F33-2G4_repeater_observer +Xiao_S3_WIO_repeater_observer +``` + +## Поддерживаемые платы мостов + +Таргеты мостов можно вывести списком из корня репозитория: + +```bash +bash posadmesh-build.sh list | grep '_repeater_bridge_espnow' +bash posadmesh-build.sh list | grep '_repeater_observer_espnow' +``` + +Типичные примеры: + +```text +heltec_v4_repeater_bridge_espnow +heltec_v4_repeater_observer_espnow +Station_G2_repeater_bridge_espnow +Station_G2_repeater_observer_espnow +T_Beam_S3_Supreme_SX1262_repeater_bridge_espnow +T_Beam_S3_Supreme_SX1262_repeater_observer_espnow +``` + +## Поддерживаемые платы `repeater_observer_mqtt_bridge` + +Для каждой платы обсервера определён вариант сборки `mqtt_bridge`. + +Список доступных таргетов: + +```bash +bash posadmesh-build.sh list | grep '_repeater_observer_mqtt_bridge' +``` + +Прошивка моста предназначена для локального использования ESP-NOW-моста между близлежащими репитерами. Это не мост MQTT-over-WAN и не VPN-мост. + +## Поддерживаемые платы `companion_radio_wifi` + +Это полные имена окружений PlatformIO, используемые для локальных сборок из исходников и для именования артефактов релизов. + +```text +heltec_tracker_v2_companion_radio_wifi +Heltec_v2_companion_radio_wifi +Heltec_v3_companion_radio_wifi +heltec_v4_companion_radio_wifi +heltec_v4_tft_companion_radio_wifi +Heltec_WSL3_companion_radio_wifi +LilyGo_TBeam_1W_companion_radio_wifi +LilyGo_TLora_V2_1_1_6_companion_radio_wifi +nibble_screen_connect_companion_radio_wifi +RAK_3112_companion_radio_wifi +Station_G2_companion_radio_wifi +T_Beam_S3_Supreme_SX1262_companion_radio_wifi +ThinkNode_M2_companion_radio_wifi +ThinkNode_M5_companion_radio_wifi +Xiao_S3_LoRa2021F33-2G4_companion_radio_wifi +Xiao_S3_WIO_companion_radio_wifi +``` + +## CLI companion Wi-Fi + +Текущие сборки companion Wi-Fi поддерживают сохраняемые команды восстановления Wi-Fi: + +- откройте монитор последовательного порта на скорости `115200` бод +- перезагрузите устройство +- в течение первых 8 секунд после загрузки зажмите пользовательскую кнопку, чтобы войти в `CLI Rescue` +- дождитесь `========= CLI Rescue =========` +- затем выполните приведённые ниже команды восстановления Wi-Fi из монитора последовательного порта + +```text +get wifi.status +get wifi.ssid +get wifi.powersaving +set wifi.ssid +set wifi.pwd +set wifi.powersaving none|min|max +``` diff --git a/posadmesh-docs/migration.md b/posadmesh-docs/migration.md new file mode 100644 index 00000000..6e73c814 --- /dev/null +++ b/posadmesh-docs/migration.md @@ -0,0 +1,121 @@ +# Миграция с `xJARiD/MeshCore` + +Эта страница описывает переход с предыдущей прошивки, опубликованной по адресу: + +- `https://github.com/xJARiD/MeshCore` + +на прошивку PosadMesh, опубликованную по адресу: + +- `https://git.meshinfo.ru/shade/MeshCore-Posadmesh` + +Эти шаги предназначены для существующих пользователей репитеров, переходящих на новую прошивку PosadMesh `*_repeater_observer`. + +Начните с загрузки подходящего релиза PosadMesh для вашей платы: + +- [Скачивание и прошивка релизов](./releases.md) + +## Краткий ответ + +Сначала попробуйте обычную прошивку, без стирания устройства. После прошивки заново введите текущие настройки Wi-Fi и MQTT PosadMesh, перечисленные ниже. + +Полное стирание с прошивкой (erase-flash) выполняйте только в том случае, если репитер по-прежнему ведёт себя странно после перепрошивки, повторного применения настроек и перезагрузки. + +## Перед началом + +- новую прошивку можно установить без предварительного стирания +- после прошивки потребуется заново применить настройки Wi-Fi и некоторые настройки MQTT +- это можно сделать как через последовательный CLI, так и через приложение companion + +Старые настройки MQTT-моста из `xJARiD/MeshCore` не переносятся в текущие настройки MQTT-аплинка PosadMesh. Введите заново те настройки, которые вы используете сейчас, вместо того чтобы рассчитывать на перенос старых значений брокера/анализатора. + +## Обязательная настройка после прошивки + +После установки новой прошивки задайте: + +- `set wifi.ssid ` +- `set wifi.pwd ` +- `set mqtt.iata ` +- `set mqtt.status on` + +## Необязательные настройки + +Если вы ранее использовали метаданные владельца, также задайте: + +- `set mqtt.owner <64-hex-char-public-key>` +- `set mqtt.email ` + +Если вы ранее отключали публикацию TX, также задайте: + +- `set mqtt.tx off` + +## Последний шаг + +После применения настроек: + +- `reboot` + +После перезагрузки репитер должен работать. + +## Проверка работы + +Убедитесь, что репитер запущен и подключён, с помощью: + +- `get wifi.status` +- `get mqtt.status` + +## Проверка в веб-консоли + +Если на вашей плате включена веб-панель, проверить можно также через локальную страницу администратора по HTTPS. + +Включите и проверьте её с помощью: + +- `set web on` +- `get web.status` + +Затем откройте: + +- `https:///` + +Примечания: + +- игнорируйте предупреждение браузера о самоподписанном сертификате +- войдите, используя пароль администратора +- это тот же пароль администратора, который используется для доступа администратора через приложение companion + +## Устаревшие команды MQTT + +Некоторые CLI-команды MQTT из предыдущей прошивки не применяются в PosadMesh и не должны переноситься. + +PosadMesh хранит текущие настройки Wi-Fi и MQTT-аплинка отдельно от старой прошивки, поэтому старые настройки брокера и анализатора не переносятся осмысленным образом. После прошивки введите заново те настройки, которые вы используете сейчас, вместо того чтобы ожидать переноса старой конфигурации MQTT-моста. + +Следующие прежние CLI-команды MQTT больше не применяются в PosadMesh: + +- `get mqtt.server` +- `get mqtt.port` +- `get mqtt.password` +- `get mqtt.analyzer.us` +- `get mqtt.analyzer.eu` +- `get mqtt.config.valid` + +Если выполнить прошивку без предварительного стирания, некоторые старые значения могут остаться во flash-памяти в фоновом режиме. Важно то, что PosadMesh не использует эти старые значения MQTT-моста для текущей конфигурации аплинка, поэтому пользователям не следует ожидать их переноса или какого-либо эффекта. + +## Рекомендуемый порядок миграции + +1. Прошейте новую прошивку PosadMesh без стирания. +2. Откройте последовательный CLI или приложение companion. +3. Задайте `wifi.ssid`, `wifi.pwd`, `mqtt.iata` и `mqtt.status on`. +4. При необходимости задайте `mqtt.owner` и `mqtt.email`. +5. Если вы ранее отключали публикацию TX, задайте `mqtt.tx off`. +6. Перезагрузите репитер. +7. Проверьте с помощью `get wifi.status` и `get mqtt.status`. +8. При необходимости убедитесь в локальном веб-доступе с помощью `set web on`, `get web.status` и `https:///`. + +## См. также + +- [Веб-панель репитера](./web-panel.md) +- [Пользовательские команды CLI](./custom-cli.md) +- [Скачивание и прошивка релизов](./releases.md) + +## Крайняя мера + +Если репитер по-прежнему не запускается корректно после перепрошивки, повторного применения настроек и перезагрузки, крайняя мера — выполнить полное стирание с прошивкой (erase-flash), а затем настроить его заново с нуля. diff --git a/posadmesh-docs/releases.md b/posadmesh-docs/releases.md new file mode 100644 index 00000000..f2233874 --- /dev/null +++ b/posadmesh-docs/releases.md @@ -0,0 +1,209 @@ +# Загрузка и прошивка релизов + +Файлы релизов PosadMesh публикуются на: + +- + +## С чего начать + +Если вы прошиваете прошивку PosadMesh впервые, самый простой путь: + +1. Откройте . +2. Выберите прошивку, соответствующую тому, что будет делать устройство. +3. Выберите плату, точно соответствующую вашему железу. +4. Выберите версию. Флешер по умолчанию выбирает последний доступный релиз. +5. Выберите тип образа: + - `Update` для обычного обновления прошивки + - `Full Flash` для чистой прошивки полного образа +6. Нажмите `Flash Firmware`. +7. После прошивки завершите настройку через `Open MeshCore Config Panel` или `Serial Console`. + +Если вы не уверены, какой трек вам нужен, начните с `companion-wifi` для companion-устройств, подключаемых к приложению, или с `observer-posadmesh` для стационарного репитера, который должен публиковать данные в MQTT. + +## Выбор трека + +PosadMesh публикует четыре трека релизов: + +| Трек | Используйте, когда | Суффикс имени файла прошивки | +| --------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------ | +| `companion-wifi` | Нужно companion-устройство, которое подключается по Wi-Fi вместо BLE или USB. | `*_companion_radio_wifi` | +| `observer-posadmesh` | Нужен репитер с Wi-Fi и MQTT-аплинком, обычно для видимости в брокере, например в PosadMesh/CoreScope. | `*_repeater_observer` | +| `repeater-bridge-espnow` | Нужен локальный ESP-NOW мост между соседними репитерами, без MQTT-аплинка и без веб-панели PosadMesh. | `*_repeater_bridge_espnow` | +| `observer-posadmesh-bridge-espnow` | Нужно, чтобы один репитер выполнял и MQTT-аплинк, и задачи локального ESP-NOW моста. | `*_repeater_observer_espnow` | + +!!! note "Прошивка моста — это не WAN-мост" + + Треки ESP-NOW моста предназначены для соединения двух соседних репитеров, работающих с разными конфигурациями радио MeshCore, например `Australia (Narrow)` и `Australia (Mid)`. + + Треки моста не используют брокеры MQTT-аплинка для туннелирования mesh-трафика через интернет, WAN-каналы или VPN. + +## Выбор подходящего файла релиза + +Скачайте файл, соответствующий вашей плате и типу прошивки. + +Примеры: + +- `heltec_v4_companion_radio_wifi-v1.14.1-abcdef.bin` +- `heltec_v4_repeater_observer-v1.15.0-posadmesh-v2026.5.1-abcdef.bin` +- `heltec_v4_repeater_observer-v1.15.0-posadmesh-v2026.5.1-abcdef-merged.bin` +- `heltec_v4_repeater_bridge_espnow-v1.15.0-abcdef.bin` +- `heltec_v4_repeater_observer_espnow-v1.15.0-posadmesh-v2026.5.1-abcdef.bin` + +Важен префикс платы/env: + +- `*_companion_radio_wifi` +- `*_repeater_observer` +- `*_repeater_bridge_espnow` +- `*_repeater_observer_espnow` + +## Какой файл прошивать + +Если вы используете , выберите тип образа во флешере: + +- `Update` = обычное обновление прошивки +- `Full Flash` = чистая прошивка полного образа + +Если вы скачиваете файлы вручную из раздела Releases на git.meshinfo.ru, вместо этого ориентируйтесь на имя файла: + +Используйте стандартный файл `.bin`, когда обновляете существующее устройство с тем же таргетом и той же разметкой разделов. + +Используйте файл `-merged.bin`, когда нужна чистая установка после стирания flash. Это полный образ ESP32, и его следует прошивать по адресу `0x0`. + +Практическое правило: + +- `.bin` = инкрементальное обновление +- `-merged.bin` = стирание и чистая прошивка + +## Процесс прошивки + +1. Выберите прошивку. +2. Выберите плату. +3. Выберите версию. Последняя версия выбирается по умолчанию. +4. Выберите тип образа: `Update` или `Full Flash`. +5. Нажмите `Flash Firmware`. +6. После прошивки используйте `Open MeshCore Config Panel` или `Serial Console` для настройки после прошивки, такой как Wi-Fi, MQTT, мост или параметры радио. + +## Рекомендуемый флешер + +Рекомендуемый флешер: + +- + +Он имеет встроенную поддержку распространённых типов прошивок PosadMesh, а также может прошивать пользовательские файлы прошивок: + +- прошивка `companion_radio_wifi` +- прошивка `repeater_observer` +- пользовательские файлы прошивок + +Для релизов моста используйте соответствующий пункт для моста, если флешер его показывает. В противном случае скачайте файл релиза самостоятельно и прошейте его как пользовательский файл прошивки. + +Рекомендуемое использование: + +- используйте `Update`, когда обновляете существующее устройство +- используйте `Full Flash`, когда нужна чистая прошивка полного образа + +После прошивки во флешере доступны два полезных пути настройки. + +### Открыть панель конфигурации MeshCore + +Используйте `Open MeshCore Config Panel`, чтобы получить доступ к пошаговым инструментам настройки MeshCore: + +- `Repeater Setup` +- `Console` + +#### Настройка репитера + +`Repeater Setup` — это пошаговый процесс первой настройки репитера. + +Это традиционный способ настройки репитера после прошивки, включающий: + +- имя устройства +- широту и долготу +- пароли администратора и гостя +- параметры радио, включая выбор пресета +- интервал advert +- интервал flood advert +- flood max +- некоторые расширенные настройки репитера + +Начиная с `v1.2.1`, локальная веб-панель репитера также включает те же общие настройки репитера, поэтому пользователи могут выполнить первоначальную настройку там и возвращаться к ней для периодической диагностики или изменения конфигурации. На обсерверах, которым нужен максимальный запас heap, по-прежнему лучше снова отключить панель, когда вы закончили. + +#### Консоль + +`Console` — это необработанный интерфейс CLI. + +Он особенно полезен, а часто и необходим, для первоначальной настройки Wi-Fi на прошивках с поддержкой Wi-Fi: + +- `set wifi.ssid ` +- `set wifi.pwd ` + +Это относится к: + +- `companion_radio_wifi` +- `repeater_observer` +- `repeater_observer_espnow` + +### Последовательная консоль + +Используйте `Serial Console`, когда хотите подключиться по USB и вводить команды CLI напрямую. + +Флешер может подключаться к устройству по USB и отключаться от него. Он также включает готовые команды CLI, которые подставляются в поле ввода команд для упрощения настройки: + +- `set wifi.ssid` +- `set wifi.pwd` +- `get wifi.status` +- `get mqtt.status` +- `set web on` +- `get web.status` + +Для команд `set` допишите команду в поле ввода перед отправкой. Например, выберите `set wifi.ssid`, добавьте имя вашей Wi-Fi сети, затем нажмите `Send`. + +## Типовые первые шаги + +### Обсервер + +Сборки `repeater_observer` включают дополнения MQTT от PosadMesh. В зависимости от платы они также могут включать локальную веб-панель. + +Типовые первые шаги после прошивки: + +- задайте `wifi.ssid` +- задайте `wifi.pwd` +- задайте `mqtt.iata` +- проверьте `get mqtt.status` +- при желании задайте `mqtt.owner` и `mqtt.email` +- при желании включите пользовательский брокер как второй аплинк + +### Обсервер ESP-NOW + +Сборки `repeater_observer_espnow` объединяют роль обсервера с поддержкой локального ESP-NOW моста. + +Типовые первые шаги после прошивки: + +- задайте `wifi.ssid` +- задайте `wifi.pwd` +- задайте `mqtt.iata` +- проверьте `get mqtt.status` +- проверьте `get wifi.status` и запомните канал Wi-Fi, к которому выполнено подключение +- задайте `bridge.channel` в соответствии с этим каналом Wi-Fi +- задайте одинаковый `bridge.secret` на каждом узле локального ESP-NOW моста, которые должны общаться между собой + +### ESP-NOW мост репитера + +Сборки `repeater_bridge_espnow` предназначены для узлов локального ESP-NOW моста без MQTT-аплинка. + +Типовые первые шаги после прошивки: + +- настройте обычные параметры репитера для платы +- задайте нужный пресет/конфигурацию радио MeshCore +- задайте одинаковые канал моста и секрет для локальной пары моста +- убедитесь, что оба репитера моста физически находятся достаточно близко для работы ESP-NOW + +### Companion-радио Wi-Fi + +Сборки `companion_radio_wifi` предназначены для companion-устройств, которые предоставляют интерфейс приложения по Wi-Fi вместо BLE или USB. + +Типовые первые шаги после прошивки: + +- задайте учётные данные Wi-Fi +- проверьте `get wifi.status` +- подключите ваше клиентское приложение к companion-устройству по Wi-Fi diff --git a/posadmesh-docs/web-panel.md b/posadmesh-docs/web-panel.md new file mode 100644 index 00000000..5031a61e --- /dev/null +++ b/posadmesh-docs/web-panel.md @@ -0,0 +1,433 @@ +# Веб-панель репитера + +Эта страница предназначена для конечных пользователей, которые используют сборку PosadMesh `*_repeater_observer` с включённой локальной веб-панелью. + +Здесь описано, как открыть панель, что делает каждый раздел и чего ожидать при работе с ней на компьютере или мобильном устройстве. + +## С чего начать + +Для обычного первого использования: + +1. Подключите репитер к Wi-Fi. +2. Выполните `get wifi.status`, чтобы узнать его IP-адрес. +3. Откройте `https:///` в браузере. +4. Примите предупреждение о самоподписанном сертификате. +5. Войдите с паролем администратора репитера. +6. Используйте панель для настройки или диагностики. +7. По завершении выполните `set web off`, если репитеру нужен максимальный запас heap для MQTT. + +Большинству пользователей панель нужна только для первой настройки, редких изменений параметров и диагностики. Оставляйте её включённой только тогда, когда доступ из браузера стоит дополнительного расхода памяти. + +## Что это такое + +Веб-панель репитера — это локальная страница настройки по HTTPS, которую репитер отдаёт напрямую по Wi-Fi. + +Она предоставляет: + +- локальную страницу администратора за паролем по адресу `/app` +- отдельную страницу статистики и трендов по адресу `/stats` +- быстрые команды `get` для типовых проверок репитера и MQTT +- панель CLI в стиле терминала для полного доступа к CLI репитера +- редактируемые настройки репитера +- редактируемые настройки MQTT +- просмотр исторической статистики с трендами, соседями и последними событиями + +Рекомендации по эксплуатации: + +- используйте её для первоначальной настройки, редких изменений конфигурации и диагностики +- по завершении предпочитайте `set web off` на обсерверах, которым нужен максимальный запас +- это оставляет больше внутреннего heap для работы MQTT/WSS, особенно в конфигурациях с двумя брокерами + +## Типовые задачи + +### Проверить Wi-Fi и MQTT + +1. Откройте панель. +2. Нажмите `wifi.status` в Quick `get` Commands. +3. Нажмите `mqtt.status` в Quick `get` Commands. +4. Откройте `/stats` из верхней навигации для просмотра исторической статистики. + +### Изменить имя устройства + +1. Отредактируйте `Device Name`. +2. Нажмите `Save`. +3. Подтвердите сгенерированную команду и ответ в поле терминала CLI. + +### Обновить владельца или email MQTT + +1. Перейдите в `MQTT Settings`. +2. Введите новое значение. +3. Нажмите `Save`. +4. Используйте кнопку обновления, если хотите перечитать сохранённое значение из репитера. + +## Обзор скриншотов + +Скриншоты ниже показывают текущее разделение между более лёгкой страницей администратора `/app` и отдельной страницей статуса `/stats`. + +### Скриншот `/app` + +![Обзор веб-панели репитера `/app`](./_assets/repeater_web_panel_app_light.png#only-light) +![Обзор веб-панели репитера `/app`](./_assets/repeater_web_panel_app_dark.png#only-dark) + +### Скриншот `/stats` + +![Обзор веб-панели репитера `/stats`](./_assets/repeater_web_panel_stats_light.png#only-light) +![Обзор веб-панели репитера `/stats`](./_assets/repeater_web_panel_stats_dark.png#only-dark) + +## Требования + +Вам потребуется: + +- поддерживаемая сборка прошивки `*_repeater_observer` +- настроенный на репитере Wi-Fi +- репитер, подключённый к вашей локальной сети +- пароль администратора репитера + +Некоторые ограниченные по ресурсам таргеты отключают веб-панель, чтобы уложиться в лимиты flash. Если ваша плата её не поддерживает, `get web.status` не покажет её как доступную. + +## Как её открыть + +1. Подключите репитер к Wi-Fi. +2. Узнайте его IP-адрес. +3. Откройте `https:///` в браузере. +4. Примите предупреждение браузера о самоподписанном сертификате. +5. Введите пароль администратора репитера. + +Полезные команды CLI: + +- `get wifi.status`: показывает состояние Wi-Fi, IP-адрес, канал и уровень сигнала при подключении. +- `get web.status`: показывает, запущена ли веб-панель и какой URL использовать. + +Пример: + +- `https://10.33.135.208/` + +## Вход и безопасность + +- панель использует тот же пароль администратора, что и CLI репитера +- соединение идёт по HTTPS, но сертификат самоподписанный +- браузеры будут предупреждать при первом подключении +- после входа панель открывает доступ к CLI репитера +- браузер сохраняет токен сессии при переключении между `/app` и `/stats`; страница переключает представления на месте до выхода, блокировки по бездействию или перезапуска устройства +- кратковременные пропадания Wi-Fi могут прервать загрузку страницы, но не должны требовать нового входа при перезапуске веб-панели +- после перезапуска или перепрошивки устройства приложение проверяет сохранённый токен перед загрузкой настроек и возвращается к экрану входа, если сессия устарела + +Это предназначено для локального административного использования в доверенной сети, а не для открытого доступа из интернета. + +## Примечания по производительности + +Панель спроектирована так, чтобы загружаться мягче, чем более ранние версии. При входе она теперь запрашивает разделы последовательно, а не запрашивает сразу один большой bootstrap payload. + +Даже с этим изменением панель по-прежнему использует HTTPS и внутренний heap. На платах с одним или двумя брокерами WSS MQTT открытие панели уменьшает запас MQTT на время активной сессии. + +Рекомендуемая практика для развёртываний репитера: + +- включайте панель для первоначальной настройки +- используйте её снова для редких проверок или диагностики +- отключайте её командой `set web off` по завершении, чтобы у MQTT было максимально возможное свободное место + +## Навигация и действия + +У веб-консоли теперь две основные страницы: + +- `/app`: более лёгкое представление для управления и настройки +- `/stats`: текущий статус, тренды, соседи и последние события + +Обе страницы используют одну и ту же верхнюю навигацию и служебные действия. + +### `/app` + +Страница `/app` — это основная область администрирования и настройки. + +Она включает: + +- переход к `App` и `Stats` +- `Advert` +- `Start OTA` +- `Reboot` +- переключатель темы +- `Logout` + +Используйте `Start OTA` только тогда, когда собираетесь обновить прошивку. + +Если браузер может получить манифест прошивки PosadMesh flasher, страница приложения сверяет версию релиза PosadMesh из `mqtt.client_version` с опубликованными релизами MeshCore-Posadmesh. Когда доступен более новый тег релиза PosadMesh, в верхней части страницы появляется уведомление об обновлении прошивки. + +Для сборок, включающих `CLIENT_ENV`, уведомление может показывать `Update now`. Это скачивает соответствующий не объединённый `.bin` с зеркала прошивок PosadMesh flasher, показывает прогресс в баннере, загружает прошивку через веб-панель HTTPS и позволяет устройству перезагрузиться. Для этого требуются заголовки CORS на путях `/firmwares/` flasher. + +### `/stats` + +Страница `/stats` — это основное место для текущего статуса и исторического обзора. + +Она включает: + +- переход к `App` и `Stats` +- `Refresh` +- `Reboot` +- переключатель темы +- `Logout` + +## Быстрые команды "get" + +Этот раздел выполняет типовые команды только для чтения для: + +- Wi-Fi +- MQTT + +Они полезны для быстрых проверок без ввода в поле CLI. Быстрые действия MQTT включают `mqtt.status`, `mqtt.client_version`, `mqtt.iata`, `mqtt.owner` и `mqtt.email`. + +## Выполнить команду CLI + +Это небольшой терминал для CLI репитера. + +- нажмите `Enter`, чтобы выполнить команду +- история команд отображается в поле терминала ниже +- кнопки сохранения в других частях страницы также показывают здесь сгенерированную команду и ответ +- `clock` доступна здесь, если вы хотите проверить текущее время платы репитера +- аутентифицированные сессии могут выполнять те же команды CLI, которые принимает репитер + +Это позволяет легко увидеть, что именно панель отправила репитеру. + +## Настройки репитера + +В этот раздел входят: + +- Device Name +- Clock UTC +- Latitude +- Longitude +- Guest Password +- Private Key +- Advert Interval +- Flood Interval +- Scoped Flood Max +- Unscoped Flood Max +- Owner Info + +Примечания: + +- `Latitude` и `Longitude` по умолчанию равны `0.0` как заполнители +- изменение приватного ключа требует перезагрузки для применения +- кнопки обновления загружают текущее значение из репитера +- кнопки сохранения немедленно отправляют соответствующую команду CLI + +## Настройки радио и регионы + +Раздел `Radio Settings` включает селектор community-пресета радио, режим path hash и выпадающий список `Region (Australia)`, построенный из групп штатов, используемых селектором MQTT IATA. Список предустанавливается только тогда, когда у репитера уже есть соответствующий разрешённый регион `au-STATE`. Нажатие кнопки `Save` для региона создаёт и разрешает базовый регион `au` вместе с выбранным регионом штата, а затем сохраняет карту регионов командой `region save`. + +## Информация + +Этот раздел показывает: + +- `Version`: версия прошивки с датой сборки +- `Client Version`: строка версии MQTT-клиента +- `Public Key` + +## Режим Ghost Node + +Режим Ghost Node — это удобное управление на странице `/app` для репитера, который должен оставаться в Wi-Fi и MQTT, но не должен активно вести себя как ещё один репитер поблизости. + +Типичный сценарий использования: + +- MQTT-обсервер в помещении или в одном месте с другим устройством, когда соседний репитер уже выполняет работу по RF-ретрансляции +- узел, который вы хотите использовать для передачи данных в MQTT, веб-статуса и диагностики, не добавляя при этом лишний трафик ретрансляции или advert-пакеты + +Когда режим Ghost Node включён, он: + +- отключает `repeat` +- устанавливает `advert.interval` в `0` +- устанавливает `flood.advert.interval` в `0` +- оставляет работающими локальную веб-панель и функции MQTT + +Когда он отключён, панель восстанавливает предыдущие настройки repeat и advert, если она всё ещё знает их из текущей сессии браузера. Если нет, она возвращается к: + +- `repeat on` +- `advert.interval 60` +- `flood.advert.interval 12` + +Этот режим полезен, когда вы хотите, чтобы устройство наблюдало и публиковало данные, а не выступало дополнительным RF-репитером. Он не создаёт отдельную роль прошивки; это просто сгруппированный ярлык веб-панели для этих существующих настроек. + +## Настройки MQTT + +В этот раздел входят: + +- `mqtt.iata`: выбирается из подготовленного списка восточного побережья/юго-востока. +- `mqtt.owner`: публичный ключ владельца. +- `mqtt.email`: контактный email владельца. +- Брокеры MQTT: переключатель **MeshCoreTel RU** и переключатель **Custom broker**, каждый включает или отключает свой брокер. Одновременно можно включить не более двух брокеров. +- поля custom MQTT `host:port`, транспорт TCP/WSS, имя пользователя и пароль. + +`UNSET - To be configured` — значение по умолчанию для новых установок обсервера, пока не появится реальное сохранённое значение. + +Примечания: + +- когда `mqtt.iata` имеет значение `UNSET`, панель показывает в верхней части баннер с напоминанием задать его в MQTT Settings +- пока `mqtt.iata` имеет значение `UNSET`, включённые брокеры MQTT не пытаются подключиться +- текущие состояния серверов MQTT загружаются при открытии страницы +- вы можете включать или отключать каждый сервер MQTT из этой панели +- custom MQTT использует настроенные имя пользователя и пароль, а не аутентификацию JWT +- custom MQTT по умолчанию использует TCP; выберите WSS для MQTT через защищённые WebSockets с фиксированным websocket-путём `/mqtt` и корневым CA-бандлом x509 из ESP-IDF +- отключение подключённого сервера MQTT публикует retained-статус offline до отключения клиента +- изменение `mqtt.iata` с настроенного значения публикует retained-статус offline в старый топик статуса, перезапускает подключённых клиентов брокера и переподключается по новому пути топика +- одновременно можно включить не более двух брокеров MQTT + +## Обзор `/stats` + +Страница статистики загружается отдельно от `/app` и предназначена для того, чтобы основная страница администратора оставалась легче. + +Страница `/stats` сейчас показывает: + +- `Services`: MQTT, web, архив, число соседей и, когда смонтирована, ёмкость карты и архива +- необязательная сводная карточка `Environment` на всю ширину на платах, которые передают телеметрию GPS или параметров окружающей среды +- `Trends`: батарея, свободный heap, активность пакетов, сигнал, уровень шума и, когда GPS активен, спутники +- `Neighbours`: текущая таблица соседей с ID, SNR, давностью последнего приёма и давностью advert +- `Events`: текущие события загрузки/сессии + +Запланированные ротации MQTT JWT показываются как события `mqtt_token_refreshed` вместо короткой пары `mqtt_disconnected` / `mqtt_connected`. + +Для плат, которые предоставляют дополнительную телеметрию, необязательная сводная карточка `Environment` может показывать текущие значения, такие как состояние активности/fix GPS, широта, долгота, высота по GPS, напряжение, температура датчика, влажность, барометр, высота по давлению и температура MCU. + +Метрики без текущего значения скрываются, а не отображаются строками-заполнителями, поэтому карточки различаются в зависимости от платы и текущего состояния датчиков. + +Индикатор батареи `Core` предпочитает процент батареи, сообщаемый платой, когда таргет его предоставляет. На таких платах детализация индикатора показывает только текущее значение напряжения батареи в милливольтах. В противном случае он масштабирует отображаемый процент из настроенного для платы диапазона напряжения батареи и показывает этот диапазон в тексте детализации, а не предполагает фиксированную однобаночную сборку `3000-4200 mV`. + +Графики трендов загружаются последовательно, а не одним большим payload: + +1. сводка/статус +2. батарея +3. память +4. активность пакетов +5. сигнал +6. спутники, когда GPS активен + +Это снижает потребление памяти как на стороне браузера, так и на стороне устройства по сравнению с предыдущим представлением статистики внутри страницы. + +Если `web.stats` включён и архив на SD смонтирован, тренды могут восстановить архивные сводные точки после перезагрузки из последнего снимка SD. Недавние живые точки по-прежнему добавляются из истории в памяти. + +### Ёмкость истории статистики + +Выборки статистики собираются раз в минуту. + +Текущие лимиты истории в памяти: + +| Класс платы | Лимит выборок | Лимит событий | Примерная история выборок | +| --------------------------------- | ------------: | ------------: | ------------------------------ | +| Без PSRAM | `24` | `8` | Только недавняя живая история | +| Менее `4 MB` PSRAM | `240` | `96` | Около `4` часов | +| От `4 MB` до менее `8 MB` PSRAM | `480` | `192` | Около `8` часов | +| `8 MB` PSRAM или больше | `720` | `288` | Около `12` часов | + +Когда `web.stats` включён, история статистики начинает собираться с момента загрузки или с момента его включения, даже если `/stats` ещё не открывалась. Платы без PSRAM используют меньший буфер только с живыми данными, показанный выше. + +Восстановление из архива требует включённого `web.stats` и смонтированной SD-карты на платах, которые поддерживают путь архива PosadMesh. + +Если доступ к архиву пропадает во время работы, репитер периодически повторяет монтирование SD, пока `web.stats` остаётся включённым. + +Основное назначение SD-карты — позволить репитеру сохранять и восстанавливать историю статистики для `/stats`. Архив хранит быстрые файлы снимков `.latest` для быстрого восстановления и ежедневные файлы `.log` с датой в UTC для более долгой истории. Как дополнительная возможность, эти файлы также можно извлечь и просмотреть на компьютере для более глубокого ручного анализа. + +На платах без PSRAM `/stats` всё ещё может показывать недавние графики, но история меньше и не обеспечивает такого же поведения с опорой на архив, как платы с поддержкой PSRAM. + +Полезные команды CLI: + +- `set web.stats on` +- `set web.stats off` +- `get web.stats.status` + +## Использование на мобильных устройствах + +Страница адаптивная и должна нормально работать на телефоне. + +На мобильном: + +- кнопки быстрых команд сворачиваются в двухколоночную раскладку +- верхняя навигация и группы действий остаются компактными и удобными для касания +- строки ввода остаются пригодными для взаимодействия касанием +- карточки трендов при необходимости перестраиваются в одноколоночные секции + +## Дополнительные задачи + +### Start OTA + +1. Нажмите `Start OTA`. +2. Подтвердите действие. +3. Панель запускает OTA, ждёт, пока HTTP-слушатель OTA займёт порт `80`, а затем открывает возвращённый URL `http://.../update`. +4. Локальный HTTP-слушатель перенаправления на порту `80` освобождается, чтобы OTA мог занять этот порт. +5. Продолжите по обычному сценарию OTA. + +Если репитер уже подключён к Wi-Fi, OTA использует существующий адрес в локальной сети. Если Wi-Fi не подключён, репитер запускает точку доступа `MeshCore-OTA` и возвращает вместо этого адрес OTA. +Если старый слушатель перенаправления не полностью освободил порт `80`, слушатель OTA повторяет попытки примерно до 30 секунд, после чего сдаётся. + +Если более старая сборка отправляет вас на странное перенаправление после `start ota`, используйте веб-кнопку `Start OTA` для запуска обновления. Эта проблема с перенаправлением исправлена в `observer-eastmesh-v1.3.11`. + +### Purge SD + +`Purge SD` отображается красным встроенным текстом рядом с `Archive Used` в карточке Services на `/stats`. Запрашивает подтверждение, а затем выполняет аутентифицированную команду `purge sd`. + +Это удаляет файлы и каталоги из архива на смонтированной SD-карте, после чего заново создаёт пустой каталог архива `/stats` для продолжения записи во время работы. Это не стирает внутреннюю файловую систему репитера, идентичность, prefs, регионы, настройки радио или настройки MQTT. + +### Использовать историческую статистику + +1. При необходимости включите статистику командой `set web.stats on`. +2. Откройте `/stats` из верхней навигации. +3. Просмотрите `Services` для состояния архива и работы. +4. Просмотрите `Trends` для недавней истории графиков. +5. Используйте `Refresh`, чтобы перезагрузить страницу статистики. + +## Диагностика + +### Браузер предупреждает о сертификате + +Это ожидаемо. Панель использует самоподписанный сертификат, созданный для локального использования. + +### Я не могу открыть страницу + +Проверьте: + +- репитер подключён к Wi-Fi +- IP-адрес из `get wifi.status` +- `get web.status` сообщает, что панель запущена +- ваша плата/таргет прошивки поддерживает веб-панель + +### Панель открывается, но вход не удаётся + +Используйте пароль администратора репитера, а не гостевой пароль. + +### MQTT становится нестабильным при входе + +Веб-панель теперь загружает настройки раздел за разделом, чтобы снизить нагрузку при запуске, но HTTPS всё равно расходует внутренний heap. + +Проверьте: + +- включён ли один или два брокера MQTT +- `memory` до и после входа +- улучшается ли стабильность после `set web off` + +Для стационарных установок, где время работы MQTT важнее доступа из браузера, используйте панель недолго, а затем снова отключите её. + +### Открывается HTTP вместо HTTPS + +Теперь репитер перенаправляет обычные запросы `http://` на локальный URL панели `https://`. Если после перенаправления браузер всё ещё показывает проблему с подключением, откройте `https:///` напрямую и сначала примите предупреждение о самоподписанном сертификате. + +### Статистика или настройки не обновляются + +Попробуйте: + +- обновить вкладку браузера +- использовать `Refresh` на `/stats` +- выйти и войти снова +- проверить стабильность Wi-Fi с помощью `get wifi.status` + +### `/stats` недоступна + +Проверьте: + +- `get web.status` +- `get web.stats.status` +- была ли применена команда `set web.stats on` + +Если `web.stats` отключён, `/stats` останется отключённой, и запросы исторических графиков выполняться не будут. + +## См. также + +- [Пользовательские команды CLI](./custom-cli.md) +- [Скачать и прошить релизы](./releases.md) +- [Локальная сборка с uv](./local-builds.md) diff --git a/eastmesh-tools/web-heap-check.sh b/posadmesh-tools/web-heap-check.sh similarity index 95% rename from eastmesh-tools/web-heap-check.sh rename to posadmesh-tools/web-heap-check.sh index 4e28c7bf..63456ad6 100755 --- a/eastmesh-tools/web-heap-check.sh +++ b/posadmesh-tools/web-heap-check.sh @@ -15,14 +15,14 @@ # leaked MQTT clients abandoned after a heap-integrity failure. # # Usage: -# eastmesh-tools/web-heap-check.sh [--stress] [--rounds N] +# posadmesh-tools/web-heap-check.sh [--stress] [--rounds N] # # Auth: set REPEATER_PASSWORD (the admin password; the script logs in itself) or # REPEATER_TOKEN (an existing session token). If neither is set you are prompted # for the password. Tokens are never written to disk. # # Example: -# REPEATER_PASSWORD=... eastmesh-tools/web-heap-check.sh 192.168.1.50 --stress +# REPEATER_PASSWORD=... posadmesh-tools/web-heap-check.sh 192.168.1.50 --stress set -euo pipefail host="${1:-}" diff --git a/pyproject.toml b/pyproject.toml index 59f6af17..064ab2c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "meshcore-eastmesh" +name = "meshcore-posadmesh" version = "0.1.0" -description = "Python tooling for EastMesh workflows on top of MeshCore" +description = "Python tooling for PosadMesh workflows on top of MeshCore" requires-python = ">=3.11" dependencies = [ "platformio>=6.1.18,<7", diff --git a/release-notes.yml b/release-notes.yml index 4917215c..523e2a7a 100644 --- a/release-notes.yml +++ b/release-notes.yml @@ -2,8 +2,8 @@ schema_version: 1 tracks: - companion-wifi - repeater-bridge-espnow - - observer-eastmesh - - observer-eastmesh-bridge-espnow + - observer-posadmesh + - observer-posadmesh-bridge-espnow generated_from: "Adjacent git tag comparisons plus non-merge commit subjects." releases: @@ -186,7 +186,7 @@ releases: text: "Included upstream board, radio, display, and platform fixes from MeshCore dev." breaking_changes: [] - - track: observer-eastmesh + - track: observer-posadmesh version: "2026.5.1" tag: "observer-eastmesh-v2026.5.1" date: "2026-05-23" @@ -205,7 +205,7 @@ releases: breaking_changes: - "The EastMesh MQTT release tag format and PlatformIO target suffix changed to Observer naming." - - track: observer-eastmesh + - track: observer-posadmesh version: "2026.6.0" tag: "observer-eastmesh-v2026.6.0" date: "2026-06-03" @@ -232,7 +232,7 @@ releases: text: "Merged upstream MeshCore dev updates including sensor handling, RAK4631 BME680/BSEC support, documentation refreshes, and board/platform fixes." breaking_changes: [] - - track: observer-eastmesh + - track: observer-posadmesh version: "2026.6.1" tag: "observer-eastmesh-v2026.6.1" date: "2026-06-07" @@ -250,7 +250,7 @@ releases: text: "Updated EastMesh custom CLI docs for the new `flood.max.advert` command." breaking_changes: [] - - track: observer-eastmesh + - track: observer-posadmesh version: "2026.6.2" tag: "observer-eastmesh-v2026.6.2" date: "2026-06-07" @@ -286,7 +286,7 @@ releases: text: "Hardened OTA startup by binding before reporting success and falling back to alternate ports when port 80 is still being released." breaking_changes: [] - - track: observer-eastmesh + - track: observer-posadmesh version: "2026.6.3" tag: "observer-eastmesh-v2026.6.3" date: "2026-06-09" @@ -310,7 +310,7 @@ releases: text: "Documented custom MQTT TCP/WSS configuration in the web panel and custom CLI guides." breaking_changes: [] - - track: observer-eastmesh + - track: observer-posadmesh version: "2026.6.4" tag: "observer-eastmesh-v2026.6.4" date: "2026-06-23" @@ -339,7 +339,7 @@ releases: - "`get mqtt.status` output changed from per-broker fields to `p:`/`s:` broker slots; anything parsing the previous format must be updated." - "Saved LetsMesh EU/US broker selections are cleared once on upgrade; re-enable a broker such as MeshMapper if a second uplink is required." - - track: observer-eastmesh + - track: observer-posadmesh version: "2026.6.5" tag: "observer-eastmesh-v2026.6.5" date: "2026-06-26" @@ -363,7 +363,7 @@ releases: text: "Documented the Waev broker in the README and custom CLI guides." breaking_changes: [] - - track: observer-eastmesh + - track: observer-posadmesh version: "2026.6.6" tag: "observer-eastmesh-v2026.6.6" date: "2026-06-27" @@ -375,7 +375,7 @@ releases: text: "Added `packets_sent` and `packets_received` counters to the MQTT `status` payload `stats` object, reporting cumulative radio TX/RX packet totals to align with the Waev/MeshMapper status schema." breaking_changes: [] - - track: observer-eastmesh + - track: observer-posadmesh version: "2026.7.0" tag: "observer-eastmesh-v2026.7.0" date: "2026-07-19" @@ -397,7 +397,7 @@ releases: breaking_changes: - "The saved settings layout changed; devices upgrading from 2026.6.6 or earlier may need EastMesh-specific settings (such as fan control and bridge peer) and recently added radio settings reconfigured." - - track: observer-eastmesh + - track: observer-posadmesh version: "2026.8.0" tag: "observer-eastmesh-v2026.8.0" date: "2026-08-09" @@ -416,7 +416,7 @@ releases: breaking_changes: - "Saved settings move to the new `prefs.json` format on first boot; downgrading to earlier firmware afterwards reverts the device to the old (pre-migration) settings file." - - track: observer-eastmesh + - track: observer-posadmesh version: "2026.8.1" tag: "observer-eastmesh-v2026.8.1" date: "2026-08-09" @@ -431,7 +431,7 @@ releases: text: "Made a failed or absent SH1106 display boot the node headless instead of crash-looping: display operations are now safe no-ops when the boot-time initialisation did not succeed." breaking_changes: [] - - track: observer-eastmesh + - track: observer-posadmesh version: "2026.8.2" tag: "observer-eastmesh-v2026.8.2" date: "2026-08-15" @@ -458,7 +458,7 @@ releases: text: "Adopted upstream's SH1106 display address fallback (0x3C/0x3D) so board revisions with the other SA0 strap setting still get a working display, while keeping the EastMesh T-Beam S3 Supreme bring-up order and headless fallback." breaking_changes: [] - - track: observer-eastmesh + - track: observer-posadmesh version: "2026.8.3" tag: "observer-eastmesh-v2026.8.3" date: "2026-08-23" @@ -485,7 +485,46 @@ releases: text: "Added `eastmesh-tools/web-heap-check.sh` to read heap and service health over the HTTPS API and optionally stress the web panel with browser-style bursts." breaking_changes: [] - - track: observer-eastmesh-bridge-espnow + - track: observer-posadmesh + version: "2026.10.0" + tag: "observer-posadmesh-v2026.10.0" + date: "2026-10-07" + previous_version: "2026.8.3" + summary: "Полный рефакторинг под PosadMesh: переименованы папки, скрипты, треки и теги, документация переведена на русский, удалены брокеры waev/meshmapper/letsmesh, репозиторий переехал на git.meshinfo.ru с локальной сборкой релизов, база обновлена до upstream MeshCore v1.17.1." + changes: + - type: changed + area: upstream + text: "База прошивки обновлена до upstream MeshCore v1.17.1." + - type: changed + area: release + text: "Проект целиком переименован в PosadMesh: папки и скрипты (`eastmesh-docs` → `posadmesh-docs`, `eastmesh-tools` → `posadmesh-tools`, `eastmesh-build.sh` → `posadmesh-build.sh`), треки релизов (`observer-eastmesh*` → `observer-posadmesh*`), форматы тегов (`observer-posadmesh-v*`, `observer-posadmesh-bridge-espnow-v*`, `observer-posadmesh-bridge-mqtt-v*`), суффикс версии сборки (`-posadmesh-`) и переменная версии релиза (`EASTMESH_VERSION` → `POSADMESH_VERSION`)." + - type: docs + area: docs + text: "Документация переведена на русский язык: `posadmesh-docs/*.md`, `README.md`, `RELEASE.md` и `AGENTS.md`. Команды, их вывод, код, пути и идентификаторы оставлены без перевода." + - type: changed + area: release + text: "Репозиторий переехал на , workflow GitHub Actions удалены: прошивки собираются локально через `posadmesh-build.sh`, а теги и релизы создаются вручную в веб-интерфейсе git.meshinfo.ru." + - type: changed + area: mqtt + text: "Удалены брокеры `waev`, `meshmapper` и `letsmesh-eu`/`letsmesh-us` — из прошивки, веб-панели и документации; остались `mqtt.meshcoretel` и `mqtt.custom`. Сохранённые выборы удалённых брокеров сбрасываются один раз при обновлении." + - type: fixed + area: cli + text: "Исправлена команда `set mqtt.meshcoretel on|off`: обработчик сравнивался со строкой `set mqtt.mqtt.meshcoretel `, из-за чего команда не срабатывала. Заодно алиас `set mqtt.meshcoretel.ru on|off` читает значение по правильному смещению, а не всегда выключает брокер." + - type: fixed + area: mqtt + text: "Исправлено молчаливое отключение собственного брокера: `setEndpointEnabled` маскировал сохранённую маску брокеров значением `0x07` и терял бит custom при переключении любого другого брокера." + - type: fixed + area: web + text: "Исправлена разметка страницы логина: незакрытый тег `, workflow GitHub Actions удалены: прошивки собираются локально через `posadmesh-build.sh`, а теги и релизы создаются вручную в веб-интерфейсе git.meshinfo.ru." + - type: changed + area: mqtt + text: "Удалены брокеры `waev`, `meshmapper` и `letsmesh-eu`/`letsmesh-us` — из прошивки, веб-панели и документации; остались `mqtt.meshcoretel` и `mqtt.custom`. Сохранённые выборы удалённых брокеров сбрасываются один раз при обновлении." + - type: fixed + area: cli + text: "Исправлена команда `set mqtt.meshcoretel on|off`: обработчик сравнивался со строкой `set mqtt.mqtt.meshcoretel `, из-за чего команда не срабатывала. Заодно алиас `set mqtt.meshcoretel.ru on|off` читает значение по правильному смещению, а не всегда выключает брокер." + - type: fixed + area: mqtt + text: "Исправлено молчаливое отключение собственного брокера: `setEndpointEnabled` маскировал сохранённую маску брокеров значением `0x07` и терял бит custom при переключении любого другого брокера." + - type: fixed + area: web + text: "Исправлена разметка страницы логина: незакрытый тег `= 0 && PIN_BOARD_SCL >= 0 Wire.begin(PIN_BOARD_SDA, PIN_BOARD_SCL); diff --git a/src/helpers/MeshadventurerBoard.h b/src/helpers/MeshadventurerBoard.h index 0325161d..65e11102 100644 --- a/src/helpers/MeshadventurerBoard.h +++ b/src/helpers/MeshadventurerBoard.h @@ -15,6 +15,8 @@ #include "ESP32Board.h" +#include + class MeshadventurerBoard : public ESP32Board { public: @@ -33,6 +35,34 @@ public: } } + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are held on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + void powerOff() override { + // TODO: re-enable this when there is a definite wake-up source pin: + // enterDeepSleep(0); + } + uint16_t getBattMilliVolts() override { analogReadResolution(12); diff --git a/src/helpers/NetworkPrefs.cpp b/src/helpers/NetworkPrefs.cpp index f48a48e9..e602e483 100644 --- a/src/helpers/NetworkPrefs.cpp +++ b/src/helpers/NetworkPrefs.cpp @@ -9,9 +9,9 @@ namespace { -constexpr const char* kDefaultNtpServer1 = "au.pool.ntp.org"; -constexpr const char* kDefaultNtpServer2 = "time.google.com"; -constexpr const char* kDefaultNtpServer3 = "time.cloudflare.com"; +constexpr const char* kDefaultNtpServer1 = "0.ru.pool.ntp.org"; +constexpr const char* kDefaultNtpServer2 = "1.ru.pool.ntp.org"; +constexpr const char* kDefaultNtpServer3 = "2.ru.pool.ntp.org"; struct LegacyWebPrefsV1 { uint32_t magic; diff --git a/src/helpers/NetworkPrefs.h b/src/helpers/NetworkPrefs.h index 0b81e977..c7ba5750 100644 --- a/src/helpers/NetworkPrefs.h +++ b/src/helpers/NetworkPrefs.h @@ -28,6 +28,7 @@ public: const char* legacy_wifi_ssid = nullptr, const char* legacy_wifi_pwd = nullptr); static bool save(FILESYSTEM* fs, const NetworkPrefs& prefs); + static const char* defaultNtpServer(uint8_t index); static constexpr uint32_t magicValue() { return kMagic; } private: diff --git a/src/helpers/NetworkService.cpp b/src/helpers/NetworkService.cpp index 76109b03..a387e1cd 100644 --- a/src/helpers/NetworkService.cpp +++ b/src/helpers/NetworkService.cpp @@ -6,6 +6,7 @@ #if defined(ESP_PLATFORM) #include + #include #include #include #include diff --git a/src/helpers/NetworkService.h b/src/helpers/NetworkService.h index e59db894..0d58b357 100644 --- a/src/helpers/NetworkService.h +++ b/src/helpers/NetworkService.h @@ -28,14 +28,15 @@ public: const char* getWifiSSID() const { return _prefs.wifi_ssid; } bool setWifiPowerSave(const char* mode); const char* getWifiPowerSave() const; - bool setNtpServer(uint8_t index, const char* server); - const char* getNtpServer(uint8_t index) const; void formatWifiStatusReply(char* reply, size_t reply_size) const; void reconnectWifi(); void forceReconnect(); // Gateway watchdog state, as reported by `get wifi.status` (gw:ok|lost wd:). bool isGatewayReachable() const; uint16_t getWatchdogReconnectCount() const; + // index 1..3; "default" restores the built-in server for that slot + bool setNtpServer(uint8_t index, const char* server); + const char* getNtpServer(uint8_t index) const; bool isWifiConnected() const override; bool hasTimeSync() const override { return _have_time_sync; } @@ -60,6 +61,7 @@ private: bool _have_time_sync; int _last_wifi_status; unsigned long _last_wifi_attempt; + time_t _last_time_sync; #if defined(ESP_PLATFORM) // Connectivity watchdog: _wd_gateway_seen and _wd_probe_pending are shared with // the lwIP tcpip thread; _wd_gateway_ip is only written while no probe is pending. diff --git a/src/helpers/StatsHistory.cpp b/src/helpers/StatsHistory.cpp index 8740ec96..7c631139 100644 --- a/src/helpers/StatsHistory.cpp +++ b/src/helpers/StatsHistory.cpp @@ -211,6 +211,10 @@ bool buildPointValue(const HistorySample& sample, const HistorySample* previous, value = static_cast(sample.battery_mv); return true; } + if (strcmp(series, "core0_util") == 0) { + value = static_cast(sample.core0_util_pct); + return true; + } if (strcmp(series, "memory") == 0) { value = static_cast(sample.heap_free); return true; @@ -224,13 +228,25 @@ bool buildPointValue(const HistorySample& sample, const HistorySample* previous, return true; } if (strcmp(series, "packets") == 0) { - if (previous == nullptr) { - value = 0; - return true; + if (previous == nullptr || sample.uptime_secs < previous->uptime_secs) { + return false; } - const uint32_t curr_total = sample.packets_sent + sample.packets_recv; - const uint32_t prev_total = previous->packets_sent + previous->packets_recv; - value = static_cast(curr_total >= prev_total ? (curr_total - prev_total) : 0); + const uint32_t d_recv = sample.packets_recv - previous->packets_recv; + const uint32_t d_sent = sample.packets_sent - previous->packets_sent; + value = static_cast(d_recv + d_sent); + return true; + } + if (strcmp(series, "error_rate") == 0) { + if (previous == nullptr || sample.uptime_secs < previous->uptime_secs) { + return false; + } + const uint32_t d_errors = sample.recv_errors - previous->recv_errors; + const uint32_t d_recv = sample.packets_recv - previous->packets_recv; + const uint32_t total = d_errors + d_recv; + if (total == 0) { + return false; + } + value = static_cast((d_errors * 1000u) / total); return true; } if (strcmp(series, "voltage") == 0) { @@ -296,12 +312,18 @@ const char* seriesTitle(const char* series) { if (strcmp(series, "battery") == 0) { return "Battery"; } + if (strcmp(series, "core0_util") == 0) { + return "Core0 Util"; + } if (strcmp(series, "memory") == 0) { return "Heap Free"; } if (strcmp(series, "packets") == 0) { return "Packet Activity"; } + if (strcmp(series, "error_rate") == 0) { + return "Error Rate"; + } if (strcmp(series, "signal") == 0) { return "Signal"; } @@ -339,12 +361,18 @@ const char* seriesUnit(const char* series) { if (strcmp(series, "battery") == 0) { return "mV"; } + if (strcmp(series, "core0_util") == 0) { + return "pct"; + } if (strcmp(series, "memory") == 0) { return "bytes"; } if (strcmp(series, "packets") == 0) { return "pkts"; } + if (strcmp(series, "error_rate") == 0) { + return "per_mille"; + } if (strcmp(series, "signal") == 0) { return "rssi_x4"; } @@ -621,9 +649,10 @@ bool StatsHistory::parseSummaryLine(const char* line, HistorySample& sample) con long gps_lon_e6 = 0; unsigned sensor_flags = 0; unsigned gps_satellites = 0; + unsigned core0_util_pct = 0; int parsed = sscanf(line, - "%lu,%lu,%u,%u,%d,%d,%d,%lu,%lu,%lu,%lu,%u,%u,%u,%u,%u,%u,%u,%d,%d,%u,%u,%d,%d,%ld,%ld,%u,%u", + "%lu,%lu,%u,%u,%d,%d,%d,%lu,%lu,%lu,%lu,%u,%u,%u,%u,%u,%u,%u,%d,%d,%u,%u,%d,%d,%ld,%ld,%u,%u,%u", &epoch_secs, &uptime_secs, &battery_mv, @@ -651,13 +680,15 @@ bool StatsHistory::parseSummaryLine(const char* line, HistorySample& sample) con &gps_lat_e6, &gps_lon_e6, &sensor_flags, - &gps_satellites); - if (parsed == 28) { + &gps_satellites, + &core0_util_pct); + if (parsed == 29 || parsed == 28) { memset(&sample, 0, sizeof(sample)); sample.epoch_secs = static_cast(epoch_secs); sample.uptime_secs = static_cast(uptime_secs); sample.packets_sent = static_cast(packets_sent); sample.packets_recv = static_cast(packets_recv); + sample.recv_errors = static_cast(recv_errors); sample.heap_free = static_cast(heap_free); sample.heap_min = static_cast(heap_free); sample.psram_free = static_cast(psram_free); @@ -665,7 +696,6 @@ bool StatsHistory::parseSummaryLine(const char* line, HistorySample& sample) con sample.battery_mv = static_cast(battery_mv); sample.queue_len = static_cast(queue_len); sample.error_flags = static_cast(error_flags); - sample.recv_errors = static_cast(recv_errors); sample.neighbour_count = static_cast(neighbour_count); sample.direct_dups = static_cast(direct_dups); sample.flood_dups = static_cast(flood_dups); @@ -685,6 +715,7 @@ bool StatsHistory::parseSummaryLine(const char* line, HistorySample& sample) con sample.gps_satellites = static_cast(gps_satellites); sample.flags = static_cast(flags); sample.battery_pct = -1; + sample.core0_util_pct = (parsed == 29) ? static_cast(core0_util_pct) : 0; return true; } @@ -1078,7 +1109,7 @@ void StatsHistory::flushSummaryLog() { char line[384]; snprintf(line, sizeof(line), - "%lu,%lu,%u,%u,%d,%d,%d,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%d,%d,%u,%u,%d,%d,%ld,%ld,%u,%u\n", + "%lu,%lu,%u,%u,%d,%d,%d,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%d,%d,%u,%u,%d,%d,%ld,%ld,%u,%u,%u\n", static_cast(latest.epoch_secs), static_cast(latest.uptime_secs), static_cast(latest.battery_mv), @@ -1106,7 +1137,8 @@ void StatsHistory::flushSummaryLog() { static_cast(latest.gps_lat_e6), static_cast(latest.gps_lon_e6), static_cast(latest.sensor_flags), - static_cast(latest.gps_satellites)); + static_cast(latest.gps_satellites), + static_cast(latest.core0_util_pct)); File latest_file = openArchiveWriteWithRecovery(_archive, kSummaryLatestPath); if (!latest_file) { @@ -1266,7 +1298,7 @@ bool StatsHistory::buildSeriesJson(const char* series, char* buffer, size_t buff bool have_current_value = false; getSampleFromOldest(_sample_count - 1, sample); - if (strcmp(series, "packets") == 0 && _sample_count >= 2) { + if ((strcmp(series, "packets") == 0 || strcmp(series, "error_rate") == 0) && _sample_count >= 2) { getSampleFromOldest(_sample_count - 2, previous); have_current_value = buildPointValue(sample, &previous, series, current_value); } else { @@ -1320,26 +1352,48 @@ bool StatsHistory::buildSeriesJson(const char* series, char* buffer, size_t buff offset = static_cast(header_written); emitted = 0; size_t valid_emitted = 0; - have_previous = false; - for (size_t i = 0; i < _sample_count && emitted < points; i += step, ++emitted) { - if (!getSampleFromOldest(i, sample)) { - break; - } - int value = 0; - const bool have_value = buildPointValue(sample, have_previous ? &previous : nullptr, series, value); - if (have_value) { - offset += snprintf(&buffer[offset], buffer_size - offset, - "%s[%lu,%d]", - valid_emitted == 0 ? "" : ",", - static_cast(sample.uptime_secs), - value); - valid_emitted++; - if (offset + 24 >= buffer_size) { - break; + if (strcmp(series, "packets") == 0) { + if (_sample_count >= 1 && getSampleFromOldest(0, previous)) { + for (size_t i = step; i < _sample_count && emitted < points; i += step, ++emitted) { + getSampleFromOldest(i, sample); + if (sample.uptime_secs > previous.uptime_secs) { + const uint32_t rx = sample.packets_recv - previous.packets_recv; + const uint32_t tx = sample.packets_sent - previous.packets_sent; + offset += snprintf(&buffer[offset], buffer_size - offset, + "%s[%lu,%d,%d]", + valid_emitted == 0 ? "" : ",", + static_cast(sample.uptime_secs), + static_cast(rx), static_cast(tx)); + valid_emitted++; + if (offset + 40 >= buffer_size) { + break; + } + } + previous = sample; } } - previous = sample; - have_previous = true; + } else { + have_previous = false; + for (size_t i = 0; i < _sample_count && emitted < points; i += step, ++emitted) { + if (!getSampleFromOldest(i, sample)) { + break; + } + int value = 0; + const bool have_value = buildPointValue(sample, have_previous ? &previous : nullptr, series, value); + if (have_value) { + offset += snprintf(&buffer[offset], buffer_size - offset, + "%s[%lu,%d]", + valid_emitted == 0 ? "" : ",", + static_cast(sample.uptime_secs), + value); + valid_emitted++; + if (offset + 24 >= buffer_size) { + break; + } + } + previous = sample; + have_previous = true; + } } snprintf(&buffer[offset], buffer_size - offset, "]}"); diff --git a/src/helpers/StatsHistory.h b/src/helpers/StatsHistory.h index 5e19fdc7..671e5fce 100644 --- a/src/helpers/StatsHistory.h +++ b/src/helpers/StatsHistory.h @@ -10,6 +10,7 @@ struct HistorySample { uint32_t uptime_secs; uint32_t packets_sent; uint32_t packets_recv; + uint32_t recv_errors; uint32_t heap_free; uint32_t heap_min; uint32_t psram_free; @@ -17,7 +18,6 @@ struct HistorySample { uint16_t battery_mv; uint16_t queue_len; uint16_t error_flags; - uint16_t recv_errors; uint16_t neighbour_count; uint16_t direct_dups; uint16_t flood_dups; @@ -37,7 +37,7 @@ struct HistorySample { uint8_t gps_satellites; uint8_t flags; int8_t battery_pct; - uint8_t reserved; + uint8_t core0_util_pct; }; struct HistoryEvent { diff --git a/src/helpers/mqtt/MQTTCaCerts.h b/src/helpers/mqtt/MQTTCaCerts.h deleted file mode 100644 index d0c8e298..00000000 --- a/src/helpers/mqtt/MQTTCaCerts.h +++ /dev/null @@ -1,113 +0,0 @@ -#pragma once - -namespace mqtt_ca_certs { - -// EastMesh presented a Let's Encrypt E7-issued certificate until July 2026, -// when it moved to a Google Trust Services WE1 wildcard (see kCombinedPem). -static const char kEastmeshIsrgRootX1Pem[] = -R"PEM(-----BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw -TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh -cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 -WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu -ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc -h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ -0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U -A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW -T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH -B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC -B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv -KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn -OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn -jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw -qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI -rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq -hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL -ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ -3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK -NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 -ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur -TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC -jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc -oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq -4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA -mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d -emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE----- -)PEM"; - -// LetsMesh currently presents a certificate issued by Google Trust Services WE1. -static const char kLetsmeshWe1Pem[] = -R"PEM(-----BEGIN CERTIFICATE----- -MIICnzCCAiWgAwIBAgIQf/MZd5csIkp2FV0TttaF4zAKBggqhkjOPQQDAzBHMQsw -CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU -MBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIwMTQw -MDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZp -Y2VzMQwwCgYDVQQDEwNXRTEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARvzTr+ -Z1dHTCEDhUDCR127WEcPQMFcF4XGGTfn1XzthkubgdnXGhOlCgP4mMTG6J7/EFmP -LCaY9eYmJbsPAvpWo4H+MIH7MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggr -BgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU -kHeSNWfE/6jMqeZ72YB5e8yT+TgwHwYDVR0jBBgwFoAUgEzW63T/STaj1dj8tT7F -avCUHYwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzAChhhodHRwOi8vaS5wa2ku -Z29vZy9yNC5jcnQwKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL2MucGtpLmdvb2cv -ci9yNC5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwCgYIKoZIzj0EAwMDaAAwZQIx -AOcCq1HW90OVznX+0RGU1cxAQXomvtgM8zItPZCuFQ8jSBJSjz5keROv9aYsAm5V -sQIwJonMaAFi54mrfhfoFNZEfuNMSQ6/bIBiNLiyoX46FohQvKeIoJ99cx7sUkFN -7uJW ------END CERTIFICATE----- -)PEM"; - -static const char kCombinedPem[] = -R"PEM(-----BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw -TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh -cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 -WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu -ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc -h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ -0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U -A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW -T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH -B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC -B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv -KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn -OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn -jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw -qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI -rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq -hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL -ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ -3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK -NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 -ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur -TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC -jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc -oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq -4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA -mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d -emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIICnzCCAiWgAwIBAgIQf/MZd5csIkp2FV0TttaF4zAKBggqhkjOPQQDAzBHMQsw -CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU -MBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIwMTQw -MDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZp -Y2VzMQwwCgYDVQQDEwNXRTEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARvzTr+ -Z1dHTCEDhUDCR127WEcPQMFcF4XGGTfn1XzthkubgdnXGhOlCgP4mMTG6J7/EFmP -LCaY9eYmJbsPAvpWo4H+MIH7MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggr -BgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU -kHeSNWfE/6jMqeZ72YB5e8yT+TgwHwYDVR0jBBgwFoAUgEzW63T/STaj1dj8tT7F -avCUHYwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzAChhhodHRwOi8vaS5wa2ku -Z29vZy9yNC5jcnQwKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL2MucGtpLmdvb2cv -ci9yNC5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwCgYIKoZIzj0EAwMDaAAwZQIx -AOcCq1HW90OVznX+0RGU1cxAQXomvtgM8zItPZCuFQ8jSBJSjz5keROv9aYsAm5V -sQIwJonMaAFi54mrfhfoFNZEfuNMSQ6/bIBiNLiyoX46FohQvKeIoJ99cx7sUkFN -7uJW ------END CERTIFICATE----- -)PEM"; - -} // namespace mqtt_ca_certs diff --git a/src/helpers/mqtt/MQTTPrefs.cpp b/src/helpers/mqtt/MQTTPrefs.cpp index 4bc7e282..03110efd 100644 --- a/src/helpers/mqtt/MQTTPrefs.cpp +++ b/src/helpers/mqtt/MQTTPrefs.cpp @@ -28,7 +28,7 @@ void MQTTPrefsStore::setDefaults(MQTTPrefs& prefs) { prefs.legacy_wifi_powersave = 0; prefs.status_interval_ms = kFixedStatusIntervalMs; prefs.custom_port = 1883; - prefs.brokers_migrated = 1; // Fresh installs need no LetsMesh retirement migration. + prefs.brokers_migrated = 1; // Fresh installs need no one-time broker cleanup. StrHelper::strncpy(prefs.iata, MQTT_DEFAULT_IATA, sizeof(prefs.iata)); #ifdef WIFI_SSID StrHelper::strncpy(prefs.legacy_wifi_ssid, WIFI_SSID, sizeof(prefs.legacy_wifi_ssid)); @@ -80,11 +80,11 @@ bool MQTTPrefsStore::load(FILESYSTEM* fs, MQTTPrefs& prefs) { prefs.custom_transport = 0; } prefs.status_interval_ms = kFixedStatusIntervalMs; - prefs.enabled_mask &= 0x3F; + // Only the meshcoretel (0x01) and custom (0x08) brokers remain; retired broker + // selections (0x02/0x04/0x10/0x20) are dropped here. + prefs.enabled_mask &= 0x09; if (!prefs.brokers_migrated) { - // LetsMesh is retired. Clear any saved EU/US selections once so dead brokers - // stop retrying; the endpoints remain re-selectable afterwards. - prefs.enabled_mask &= ~static_cast(0x02 | 0x04); + // Persist the cleaned mask once so the removed brokers stop being retried. prefs.brokers_migrated = 1; save(fs, prefs); } diff --git a/src/helpers/mqtt/MQTTUplink.cpp b/src/helpers/mqtt/MQTTUplink.cpp index 8d609a7c..ed98e489 100644 --- a/src/helpers/mqtt/MQTTUplink.cpp +++ b/src/helpers/mqtt/MQTTUplink.cpp @@ -1,5 +1,4 @@ #include "MQTTUplink.h" -#include "MQTTCaCerts.h" #ifdef WITH_MQTT_UPLINK @@ -25,15 +24,15 @@ extern "C" esp_err_t esp_crt_bundle_attach(void* conf); #include #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.14.1" + #define FIRMWARE_VERSION "v1.17.1-posadmesh-meshcoretel" #endif #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "20 Mar 2026" + #define FIRMWARE_BUILD_DATE "28 Aug 2026" #endif #ifndef CLIENT_VERSION - #define CLIENT_VERSION "eastmesh-observer" + #define CLIENT_VERSION "posadmesh-meshcoretel" #endif #ifndef CLIENT_ENV @@ -127,17 +126,10 @@ void logMqttMemorySnapshot(const char*, const char* = nullptr) { } -const MQTTUplink::BrokerSpec MQTTUplink::kBrokerSpecs[kBrokerCount] = { - {"eastmesh-au", "eastmesh-au", "mqtt2.eastmesh.au", "wss://mqtt2.eastmesh.au:443/mqtt", kEastmeshBit, false, 0}, - {"meshmapper", "meshmapper", "mqtt.meshmapper.net", "wss://mqtt.meshmapper.net:443/mqtt", kMeshmapperBit, false, 0}, - // waev enforces a shorter max token lifetime than the curated default; match its documented config. - {"waev", "waev", "mqtt.waev.app", "wss://mqtt.waev.app:443/mqtt", kWaevBit, false, 3600}, - // Retired: LetsMesh is no longer maintained. Kept selectable for legacy nodes; new nodes should use meshmapper. - {"letsmesh-eu", "letsmesh-eu", "mqtt-eu-v1.letsmesh.net", "wss://mqtt-eu-v1.letsmesh.net:443/mqtt", - kLetsmeshEuBit, false, 0}, - {"letsmesh-us", "letsmesh-us", "mqtt-us-v1.letsmesh.net", "wss://mqtt-us-v1.letsmesh.net:443/mqtt", - kLetsmeshUsBit, false, 0}, - {"custom", "custom", nullptr, nullptr, kCustomBit, true, 0}, +const MQTTUplink::BrokerSpec MQTTUplink::kBrokerSpecs[MQTTUplink::kBrokerCount] = { + {"meshcoretel", "meshcoretel", "meshcoretel.ru", "mqtt://meshcoretel.ru:1883", + kMeshcoretelBit, true, "meshcore", "meshcore"}, + {"custom", "custom", nullptr, nullptr, kCustomBit, true, nullptr, nullptr, true}, }; MQTTUplink::MQTTUplink(mesh::RTCClock& rtc, mesh::LocalIdentity& identity) @@ -174,42 +166,10 @@ bool MQTTUplink::hasEnabledBroker() const { return (_prefs.enabled_mask & kBrokerMask) != 0; } -uint8_t MQTTUplink::countEnabledBrokers() const { - uint8_t count = 0; - for (const BrokerState& broker : _brokers) { - if (broker.spec != nullptr && (_prefs.enabled_mask & broker.spec->bit) != 0) { - ++count; - } - } - return count; -} - -uint8_t MQTTUplink::countConnectedBrokers() const { - uint8_t count = 0; - for (const BrokerState& broker : _brokers) { - if (broker.spec != nullptr && broker.connected) { - ++count; - } - } - return count; -} - bool MQTTUplink::isUnsetIataValue(const char* iata) { return iata == nullptr || iata[0] == 0 || strcmp(iata, MQTT_UNSET_IATA) == 0; } -const char* MQTTUplink::brokerCaCert(const BrokerSpec& spec) { - // Note: ESP32 targets with the certificate bundle attach it instead of these PEMs; - // this fallback only applies on builds without CONFIG_MBEDTLS_CERTIFICATE_BUNDLE. - if (spec.bit == kEastmeshBit || spec.bit == kMeshmapperBit) { - // eastmesh.au switched from Let's Encrypt to a Google Trust Services WE1 - // wildcard cert (July 2026); trust both so a switch back doesn't brick uplinks. - return mqtt_ca_certs::kCombinedPem; // ISRG Root X1 + GTS WE1 - } - // waev (mqtt.waev.app) presents a Google Trust Services WE1 chain, same issuer as LetsMesh. - return mqtt_ca_certs::kLetsmeshWe1Pem; // Google Trust Services WE1 -} - const char* MQTTUplink::brokerHost(const BrokerState& broker) const { if (broker.spec == nullptr) { return ""; @@ -221,14 +181,22 @@ uint16_t MQTTUplink::brokerPort(const BrokerState& broker) const { if (broker.spec != nullptr && broker.spec->custom) { return _prefs.custom_port != 0 ? _prefs.custom_port : 1883; } - return 443; + return (broker.spec != nullptr && broker.spec->plain_tcp) ? 1883 : 443; } bool MQTTUplink::brokerUsesWss(const BrokerState& broker) const { if (broker.spec == nullptr) { return false; } - return !broker.spec->custom || _prefs.custom_transport == 1; + if (broker.spec->custom) { + return _prefs.custom_transport == 1; + } + return !broker.spec->plain_tcp; +} + +bool MQTTUplink::brokerUsesToken(const BrokerState& broker) const { + // Only the built-in WSS brokers authenticate with a JWT; the custom one uses a plain password. + return broker.spec != nullptr && !broker.spec->custom && !broker.spec->plain_tcp; } const char* MQTTUplink::brokerUri(BrokerState& broker) const { @@ -255,7 +223,15 @@ bool MQTTUplink::isBrokerConfigured(const BrokerState& broker) const { if (!broker.spec->custom) { return true; } - return _prefs.custom_host[0] != 0 && _prefs.custom_username[0] != 0 && _prefs.custom_password[0] != 0; + return _prefs.custom_host[0] != 0; // credentials are optional (anonymous brokers) +} + +void MQTTUplink::resetCustomBroker() { + for (BrokerState& broker : _brokers) { + if (broker.spec != nullptr && broker.spec->custom) { + destroyBroker(broker); + } + } } uint8_t MQTTUplink::normalizeEnabledMask(uint8_t mask) { @@ -273,130 +249,6 @@ uint8_t MQTTUplink::normalizeEnabledMask(uint8_t mask) { return normalized; } -bool MQTTUplink::hasConnectHeadroom(const BrokerState& broker) const { - const uint8_t enabled_count = countEnabledBrokers(); - const uint8_t connected_count = countConnectedBrokers(); - const bool dual_broker_attempt = enabled_count > 1 && connected_count > 0; - if (!dual_broker_attempt) { - return true; - } - - const size_t free_heap = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); - const size_t largest_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); - - if (free_heap >= kDualBrokerMinFreeHeap && largest_heap >= kDualBrokerMinLargestHeap) { - return true; - } - - MQTT_LOG("%s connect deferred: heap headroom low heap_free=%u heap_max=%u required_heap_free=%u " - "required_heap_max=%u enabled=%u connected=%u", - broker.spec != nullptr ? broker.spec->label : "-", - static_cast(free_heap), - static_cast(largest_heap), - static_cast(kDualBrokerMinFreeHeap), - static_cast(kDualBrokerMinLargestHeap), - static_cast(enabled_count), - static_cast(connected_count)); - return false; -} - -bool MQTTUplink::preflightBroker(BrokerState& broker) const { - if (broker.spec == nullptr) { - return false; - } - if (broker.spec->custom) { - return isBrokerConfigured(broker); - } - - logMqttMemorySnapshot("preflight-pre", broker.spec->label); - WiFiClientSecure client; - client.setCACert(brokerCaCert(*broker.spec)); - client.setTimeout(kWsPreflightTimeoutMillis); - if (!client.connect(brokerHost(broker), brokerPort(broker))) { - MQTT_LOG("%s preflight failed: tcp/tls connect", broker.spec->label); - client.stop(); - logMqttMemorySnapshot("preflight-failed", broker.spec->label); - return false; - } - - client.print("GET /mqtt HTTP/1.1\r\n" - "Connection: Upgrade\r\n" - "Upgrade: websocket\r\n" - "Sec-WebSocket-Version: 13\r\n" - "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" - "User-Agent: MeshCore-EastMesh\r\n" - "Host: "); - client.print(brokerHost(broker)); - client.print(":"); - client.print(brokerPort(broker)); - client.print("\r\n\r\n"); - - char response[kWsPreflightResponseSize]; - size_t used = 0; - bool header_complete = false; - unsigned long deadline_ms = millis() + kWsPreflightTimeoutMillis; - response[0] = 0; - - while (static_cast(millis() - deadline_ms) < 0) { - while (client.available() > 0) { - int c = client.read(); - if (c < 0) { - break; - } - if (used + 1 >= sizeof(response)) { - response[used] = 0; - MQTT_LOG("%s preflight failed: response header too large bytes=%u", broker.spec->label, - static_cast(used)); - client.stop(); - logMqttMemorySnapshot("preflight-failed", broker.spec->label); - return false; - } - response[used++] = static_cast(c); - response[used] = 0; - if (strstr(response, "\r\n\r\n") != nullptr) { - header_complete = true; - break; - } - } - if (header_complete) { - break; - } - if (!client.connected() && client.available() <= 0) { - break; - } - delay(10); - } - - client.stop(); - if (!header_complete) { - MQTT_LOG("%s preflight failed: incomplete response bytes=%u", broker.spec->label, static_cast(used)); - logMqttMemorySnapshot("preflight-failed", broker.spec->label); - return false; - } - - char status_line[48]; - size_t status_len = 0; - while (status_len + 1 < sizeof(status_line) && response[status_len] != 0 && response[status_len] != '\r' && - response[status_len] != '\n') { - status_line[status_len] = response[status_len]; - ++status_len; - } - status_line[status_len] = 0; - - bool ok = strncmp(response, "HTTP/", 5) == 0 && strstr(status_line, " 101") != nullptr && - containsIgnoreCase(response, "\r\nSec-WebSocket-Accept:"); - if (!ok) { - MQTT_LOG("%s preflight failed: status=\"%s\" accept=%d", broker.spec->label, status_line, - containsIgnoreCase(response, "\r\nSec-WebSocket-Accept:") ? 1 : 0); - logMqttMemorySnapshot("preflight-failed", broker.spec->label); - return false; - } - - MQTT_LOG("%s preflight ok: status=\"%s\"", broker.spec->label, status_line); - logMqttMemorySnapshot("preflight-ok", broker.spec->label); - return true; -} - void MQTTUplink::formatTopic(char* dst, size_t dst_size, const char* leaf) const { if (dst == nullptr || dst_size == 0) { return; @@ -528,7 +380,9 @@ void MQTTUplink::refreshBrokerIdentity(BrokerState& broker) { return; } if (broker.spec->custom) { - StrHelper::strncpy(broker.username, _prefs.custom_username, sizeof(broker.username)); + snprintf(broker.username, sizeof(broker.username), "%s", _prefs.custom_username); + } else if (broker.spec->username != nullptr) { + snprintf(broker.username, sizeof(broker.username), "%s", broker.spec->username); } else { snprintf(broker.username, sizeof(broker.username), "v1_%s", _device_id); } @@ -537,8 +391,8 @@ void MQTTUplink::refreshBrokerIdentity(BrokerState& broker) { } void MQTTUplink::refreshBrokerState(BrokerState& broker) { - char safe_name[40]; - makeSafeToken(board.getManufacturerName(), safe_name, sizeof(safe_name)); + char model[48]; + escapeJsonString(board.getManufacturerName(), model, sizeof(model)); char origin[80]; const char* node_name = (_node_name != nullptr && _node_name[0] != 0) ? _node_name : _device_id; escapeJsonString(node_name, origin, sizeof(origin)); @@ -553,16 +407,11 @@ void MQTTUplink::refreshBrokerState(BrokerState& broker) { snprintf(broker.offline_payload, sizeof(broker.offline_payload), "{\"status\":\"offline\",\"timestamp\":\"%s\",\"origin\":\"%s\",\"origin_id\":\"%s\",\"model\":\"%s\"," "\"firmware_version\":\"%s\",\"radio\":\"%s\",\"client_version\":\"%s\",\"repeat\":\"%s\"}", - ts, origin, _device_id, safe_name, FIRMWARE_VERSION, radio_info, client_version, + ts, origin, _device_id, model, FIRMWARE_VERSION, radio_info, client_version, _last_status.repeat_enabled ? "on" : "off"); } bool MQTTUplink::refreshToken(BrokerState& broker) { - if (broker.spec != nullptr && broker.spec->custom) { - broker.token_expires_at = 0; - return true; - } - time_t now = time(nullptr); if (now < kMinSaneEpoch) { MQTT_LOG("%s token skipped: clock not ready (%lu)", broker.spec->label, static_cast(now)); @@ -577,11 +426,10 @@ bool MQTTUplink::refreshToken(BrokerState& broker) { } } - time_t ttl = broker.spec->token_ttl_secs != 0 ? static_cast(broker.spec->token_ttl_secs) : kTokenLifetimeSecs; - time_t expires_at = now + ttl; + time_t expires_at = now + kTokenLifetimeSecs; const char* owner = _prefs.owner_public_key[0] ? _prefs.owner_public_key : nullptr; const char* email = _prefs.owner_email[0] ? _prefs.owner_email : nullptr; - if (!JWTHelper::createAuthToken(*_identity, brokerHost(broker), now, expires_at, broker.token, kBrokerTokenSize, + if (!JWTHelper::createAuthToken(*_identity, broker.spec->host, now, expires_at, broker.token, kBrokerTokenSize, owner, email)) { freeScratchBuffer(broker.token); broker.token = nullptr; @@ -589,9 +437,8 @@ bool MQTTUplink::refreshToken(BrokerState& broker) { return false; } broker.token_expires_at = expires_at; - MQTT_LOG("%s token ready exp=%lu ttl=%ld owner=%s email=%s", broker.spec->label, - static_cast(expires_at), static_cast(ttl), - owner != nullptr ? "yes" : "no", email != nullptr ? "yes" : "no"); + MQTT_LOG("%s token ready exp=%lu owner=%s email=%s", broker.spec->label, + static_cast(expires_at), owner != nullptr ? "yes" : "no", email != nullptr ? "yes" : "no"); return true; } @@ -818,17 +665,6 @@ void MQTTUplink::publishStatus(bool online) { logMqttMemorySnapshot(online ? "status-post" : "status-offline-post"); } -void MQTTUplink::scheduleBrokerRetry(BrokerState& broker, unsigned long now_ms, bool count_failure) { - if (count_failure && broker.reconnect_failures < 10) { - broker.reconnect_failures++; - } - broker.reconnect_pending = true; - broker.next_connect_attempt = now_ms + getBrokerRetryDelayMillis(broker.reconnect_failures); - MQTT_LOG("%s reconnect in %lu ms (failures=%u)", broker.spec != nullptr ? broker.spec->label : "-", - getBrokerRetryDelayMillis(broker.reconnect_failures), - static_cast(broker.reconnect_failures)); -} - void MQTTUplink::handleMqttEvent(void* handler_args, esp_event_base_t, int32_t event_id, void* event_data) { auto* broker = static_cast(handler_args); if (broker == nullptr) { @@ -855,7 +691,14 @@ void MQTTUplink::handleMqttEvent(void* handler_args, esp_event_base_t, int32_t e static_cast(WiFi.status()), WiFi.RSSI(), connected_for_ms); logMqttMemorySnapshot("disconnected", broker->spec->label); if (!broker->reconnect_pending) { - scheduleBrokerRetry(*broker, now_ms, true); + if (broker->reconnect_failures < 10) { + broker->reconnect_failures++; + } + broker->reconnect_pending = true; + broker->next_connect_attempt = now_ms + getBrokerRetryDelayMillis(broker->reconnect_failures); + MQTT_LOG("%s reconnect in %lu ms (failures=%u)", broker->spec->label, + getBrokerRetryDelayMillis(broker->reconnect_failures), + static_cast(broker->reconnect_failures)); } break; case MQTT_EVENT_ERROR: @@ -874,7 +717,14 @@ void MQTTUplink::handleMqttEvent(void* handler_args, esp_event_base_t, int32_t e MQTT_LOG("%s wifi_status=%d rssi=%d", broker->spec->label, static_cast(WiFi.status()), WiFi.RSSI()); logMqttMemorySnapshot("error", broker->spec->label); if (!broker->reconnect_pending) { - scheduleBrokerRetry(*broker, now_ms, true); + if (broker->reconnect_failures < 10) { + broker->reconnect_failures++; + } + broker->reconnect_pending = true; + broker->next_connect_attempt = now_ms + getBrokerRetryDelayMillis(broker->reconnect_failures); + MQTT_LOG("%s reconnect in %lu ms (failures=%u)", broker->spec->label, + getBrokerRetryDelayMillis(broker->reconnect_failures), + static_cast(broker->reconnect_failures)); } break; case MQTT_EVENT_BEFORE_CONNECT: @@ -905,17 +755,19 @@ void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) { return; } - time_t now = time(nullptr); unsigned long now_ms = millis(); - if (!broker.spec->custom && broker.client != nullptr && broker.token_expires_at > 0 && - now + kTokenRefreshSlackSecs >= broker.token_expires_at) { - _token_refresh_count++; - _token_refresh_active_until_ms = now_ms + kTokenRefreshEventWindowMs; - MQTT_LOG("%s token refresh reconnect count=%lu", broker.spec->label, - static_cast(_token_refresh_count)); - destroyBroker(broker, false); + if (!broker.spec->plain_tcp) { + time_t now = time(nullptr); + if (broker.client != nullptr && broker.token_expires_at > 0 && now + kTokenRefreshSlackSecs >= broker.token_expires_at) { + _token_refresh_count++; + _token_refresh_active_until_ms = now_ms + kTokenRefreshEventWindowMs; + MQTT_LOG("%s token refresh reconnect count=%lu", broker.spec->label, + static_cast(_token_refresh_count)); + destroyBroker(broker, false); + } } + if (broker.client != nullptr) { if (broker.connected) { return; @@ -923,26 +775,7 @@ void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) { if (!broker.reconnect_pending || now_ms < broker.next_connect_attempt) { return; } - if (!allow_new_connect) { - return; - } - broker.last_connect_attempt = now_ms; - broker.reconnect_pending = false; - if (!preflightBroker(broker)) { - scheduleBrokerRetry(broker, now_ms, true); - return; - } - MQTT_LOG("%s mqtt reconnect requested", broker.spec->label); - logMqttMemorySnapshot("reconnect-pre", broker.spec->label); - esp_err_t reconnect_rc = esp_mqtt_client_reconnect(broker.client); - if (reconnect_rc != ESP_OK) { - MQTT_LOG("%s mqtt reconnect failed rc=0x%x", broker.spec->label, reconnect_rc); - logMqttMemorySnapshot("reconnect-failed", broker.spec->label); - scheduleBrokerRetry(broker, now_ms, true); - } else { - logMqttMemorySnapshot("reconnect-requested", broker.spec->label); - } - return; + destroyBroker(broker, false); } if (broker.next_connect_attempt != 0 && now_ms < broker.next_connect_attempt) { @@ -951,56 +784,35 @@ void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) { if (!allow_new_connect) { return; } - if (!hasConnectHeadroom(broker)) { - broker.reconnect_pending = true; - broker.next_connect_attempt = now_ms + kHeapHeadroomRetryMillis; - return; - } broker.last_connect_attempt = now_ms; broker.reconnect_pending = false; - if (!preflightBroker(broker)) { - scheduleBrokerRetry(broker, now_ms, true); - return; - } - - if (!refreshToken(broker)) { + if (!broker.spec->plain_tcp && !refreshToken(broker)) { broker.reconnect_pending = true; broker.next_connect_attempt = now_ms + kBrokerRetryBaseMillis; return; } refreshBrokerState(broker); - const char* uri = brokerUsesWss(broker) ? brokerUri(broker) : nullptr; - MQTT_LOG("%s mqtt init host=%s port=%u transport=%s path=%s uri=%s client_id=%s", broker.spec->label, brokerHost(broker), - static_cast(brokerPort(broker)), brokerUsesWss(broker) ? "wss" : "tcp", - brokerUsesWss(broker) ? "/mqtt" : "-", uri != nullptr ? uri : "-", broker.client_id); + MQTT_LOG("%s mqtt init host=%s uri=%s plain_tcp=%d client_id=%s", + broker.spec->label, broker.spec->host, broker.spec->uri, + broker.spec->plain_tcp ? 1 : 0, broker.client_id); logMqttMemorySnapshot("init-pre", broker.spec->label); esp_mqtt_client_config_t cfg = {}; #if ESP_IDF_VERSION_MAJOR >= 5 - if (brokerUsesWss(broker)) { - cfg.broker.address.uri = uri; - } else { - cfg.broker.address.hostname = brokerHost(broker); - cfg.broker.address.port = brokerPort(broker); - cfg.broker.address.transport = MQTT_TRANSPORT_OVER_TCP; - } if (broker.spec->custom) { - cfg.credentials.authentication.password = _prefs.custom_password; + cfg.broker.address.uri = brokerUri(broker); +#if defined(MQTT_USE_CRT_BUNDLE) if (brokerUsesWss(broker)) { -#if defined(MQTT_USE_CRT_BUNDLE) cfg.broker.verification.crt_bundle_attach = MQTT_CRT_BUNDLE_ATTACH; -#endif } - } else { -#if defined(MQTT_USE_CRT_BUNDLE) - cfg.broker.verification.crt_bundle_attach = MQTT_CRT_BUNDLE_ATTACH; -#else - cfg.broker.verification.certificate = brokerCaCert(*broker.spec); #endif - cfg.credentials.authentication.password = broker.token; + cfg.credentials.authentication.password = _prefs.custom_password[0] ? _prefs.custom_password : nullptr; + } else if (broker.spec->plain_tcp) { + cfg.broker.address.uri = broker.spec->uri; + cfg.credentials.authentication.password = broker.spec->password; } - cfg.credentials.username = broker.username; + cfg.credentials.username = (broker.spec->custom && _prefs.custom_username[0] == 0) ? nullptr : broker.username; cfg.credentials.client_id = broker.client_id; cfg.session.keepalive = 30; cfg.session.last_will.topic = broker.status_topic; @@ -1013,15 +825,19 @@ void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) { cfg.buffer.size = 768; cfg.buffer.out_size = 1280; #else - if (brokerUsesWss(broker)) { - cfg.uri = uri; - } else { - cfg.host = brokerHost(broker); - cfg.port = brokerPort(broker); - cfg.transport = MQTT_TRANSPORT_OVER_TCP; + if (broker.spec->custom) { + cfg.uri = brokerUri(broker); +#if defined(MQTT_USE_CRT_BUNDLE) + if (brokerUsesWss(broker)) { + cfg.crt_bundle_attach = MQTT_CRT_BUNDLE_ATTACH; + } +#endif + cfg.password = _prefs.custom_password[0] ? _prefs.custom_password : nullptr; + } else if (broker.spec->plain_tcp) { + cfg.uri = broker.spec->uri; + cfg.password = broker.spec->password; } - cfg.username = broker.username; - cfg.password = broker.spec->custom ? _prefs.custom_password : broker.token; + cfg.username = (broker.spec->custom && _prefs.custom_username[0] == 0) ? nullptr : broker.username; cfg.client_id = broker.client_id; cfg.keepalive = 30; cfg.buffer_size = 768; @@ -1029,18 +845,6 @@ void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) { cfg.reconnect_timeout_ms = 10000; cfg.network_timeout_ms = 10000; cfg.disable_auto_reconnect = true; - if (broker.spec->custom && brokerUsesWss(broker)) { -#if defined(MQTT_USE_CRT_BUNDLE) - cfg.crt_bundle_attach = MQTT_CRT_BUNDLE_ATTACH; -#endif - } - if (!broker.spec->custom) { -#if defined(MQTT_USE_CRT_BUNDLE) - cfg.crt_bundle_attach = MQTT_CRT_BUNDLE_ATTACH; -#else - cfg.cert_pem = brokerCaCert(*broker.spec); -#endif - } cfg.lwt_topic = broker.status_topic; cfg.lwt_msg = broker.offline_payload; cfg.lwt_qos = 1; @@ -1219,46 +1023,18 @@ void MQTTUplink::formatStatusReply(char* reply, size_t reply_size) const { return "retry"; }; - // Only ever up to two brokers are enabled, so report the active slots instead of - // every broker. This keeps the reply short for LoRa and is stable as brokers are added. - auto format_slot = [this, &broker_state](const BrokerState* broker, char* buf, size_t buf_size) { - if (broker == nullptr || broker->spec == nullptr) { - StrHelper::strncpy(buf, "-", buf_size); - } else if (broker->spec->custom) { - snprintf(buf, buf_size, "custom:%s:%s", getCustomTransport(), broker_state(broker->spec->bit)); - } else { - snprintf(buf, buf_size, "%s:%s", broker->spec->key, broker_state(broker->spec->bit)); - } - }; - - const BrokerState* primary = nullptr; - const BrokerState* secondary = nullptr; - for (const BrokerState& broker : _brokers) { - if (broker.spec == nullptr || (_prefs.enabled_mask & broker.spec->bit) == 0) { - continue; - } - if (primary == nullptr) { - primary = &broker; - } else if (secondary == nullptr) { - secondary = &broker; - break; - } - } - - char primary_slot[40]; - char secondary_slot[40]; - format_slot(primary, primary_slot, sizeof(primary_slot)); - format_slot(secondary, secondary_slot, sizeof(secondary_slot)); - snprintf(reply, reply_size, - "> wifi:%s ntp:%s iata:%s p:%s s:%s status:%s tx:%s leaked:%lu", + "> wifi:%s ntp:%s iata:%s meshcoretel:%s custom:%s:%s status:%s tx:%s", (_network != nullptr && _network->isWifiConnected()) ? "up" : "down", (_network != nullptr && _network->hasTimeSync()) ? "up" : "wait", - _prefs.iata, primary_slot, secondary_slot, _prefs.status_enabled ? "on" : "off", - _prefs.tx_enabled ? "on" : "off", static_cast(_abandoned_client_count)); + _prefs.iata, + broker_state(kMeshcoretelBit), + getCustomTransport(), broker_state(kCustomBit), + _prefs.status_enabled ? "on" : "off", _prefs.tx_enabled ? "on" : "off"); } bool MQTTUplink::setEndpointEnabled(uint8_t bit, bool enabled) { + // Keep only the brokers that still exist; the custom bit must survive a toggle of another broker. uint8_t next_mask = _prefs.enabled_mask & kBrokerMask; if (enabled) { next_mask = normalizeEnabledMask(next_mask | bit); @@ -1378,25 +1154,22 @@ bool MQTTUplink::setCustomHost(const char* host) { } char cleaned[sizeof(_prefs.custom_host)]; memset(cleaned, 0, sizeof(cleaned)); - size_t oi = 0; - for (size_t i = 0; host[i] != 0 && oi + 1 < sizeof(cleaned); ++i) { - unsigned char c = static_cast(host[i]); - if (c <= ' ' || c == '/' || c == ':' || c == '\\') { - return false; - } - cleaned[oi++] = static_cast(c); - } - cleaned[oi] = 0; - bool changed = strcmp(cleaned, _prefs.custom_host) != 0; - StrHelper::strncpy(_prefs.custom_host, cleaned, sizeof(_prefs.custom_host)); - bool saved = savePrefs(); - if (saved && changed) { - for (BrokerState& broker : _brokers) { - if (broker.spec != nullptr && broker.spec->bit == kCustomBit) { - destroyBroker(broker); + if (strcmp(host, "-") != 0) { + size_t oi = 0; + for (size_t i = 0; host[i] != 0; ++i) { + unsigned char c = static_cast(host[i]); + if (c <= ' ' || c == '/' || c == ':' || c == '\\' || c >= 0x7F || oi + 1 >= sizeof(cleaned)) { + return false; } + cleaned[oi++] = static_cast(c); } } + const bool changed = strcmp(cleaned, _prefs.custom_host) != 0; + StrHelper::strncpy(_prefs.custom_host, cleaned, sizeof(_prefs.custom_host)); + const bool saved = savePrefs(); + if (changed) { + resetCustomBroker(); + } return saved; } @@ -1409,15 +1182,11 @@ bool MQTTUplink::setCustomPort(const char* port) { if (end == port || *end != 0 || parsed == 0 || parsed > 65535UL) { return false; } - bool changed = _prefs.custom_port != static_cast(parsed); + const bool changed = _prefs.custom_port != static_cast(parsed); _prefs.custom_port = static_cast(parsed); - bool saved = savePrefs(); - if (saved && changed) { - for (BrokerState& broker : _brokers) { - if (broker.spec != nullptr && broker.spec->bit == kCustomBit) { - destroyBroker(broker); - } - } + const bool saved = savePrefs(); + if (changed) { + resetCustomBroker(); } return saved; } @@ -1435,21 +1204,17 @@ bool MQTTUplink::setCustomTransport(const char* transport) { parsed = 0; } else if (strcasecmp(transport, "wss") == 0) { #if !defined(MQTT_USE_CRT_BUNDLE) - return false; + return false; // no certificate bundle in this build #endif parsed = 1; } else { return false; } - bool changed = _prefs.custom_transport != parsed; + const bool changed = _prefs.custom_transport != parsed; _prefs.custom_transport = parsed; - bool saved = savePrefs(); - if (saved && changed) { - for (BrokerState& broker : _brokers) { - if (broker.spec != nullptr && broker.spec->bit == kCustomBit) { - destroyBroker(broker); - } - } + const bool saved = savePrefs(); + if (changed) { + resetCustomBroker(); } return saved; } @@ -1458,16 +1223,13 @@ bool MQTTUplink::setCustomUsername(const char* username) { if (username == nullptr) { return false; } + if (strcmp(username, "-") == 0) { + username = ""; + } StrHelper::strncpy(_prefs.custom_username, username, sizeof(_prefs.custom_username)); refreshIdentityStrings(); - bool saved = savePrefs(); - if (saved) { - for (BrokerState& broker : _brokers) { - if (broker.spec != nullptr && broker.spec->bit == kCustomBit) { - destroyBroker(broker); - } - } - } + const bool saved = savePrefs(); + resetCustomBroker(); return saved; } @@ -1475,15 +1237,12 @@ bool MQTTUplink::setCustomPassword(const char* password) { if (password == nullptr) { return false; } - StrHelper::strncpy(_prefs.custom_password, password, sizeof(_prefs.custom_password)); - bool saved = savePrefs(); - if (saved) { - for (BrokerState& broker : _brokers) { - if (broker.spec != nullptr && broker.spec->bit == kCustomBit) { - destroyBroker(broker); - } - } + if (strcmp(password, "-") == 0) { + password = ""; } + StrHelper::strncpy(_prefs.custom_password, password, sizeof(_prefs.custom_password)); + const bool saved = savePrefs(); + resetCustomBroker(); return saved; } @@ -1523,7 +1282,7 @@ const char* MQTTUplink::getAggregateBrokerState() const { MQTTUplink::MQTTUplink(mesh::RTCClock&, mesh::LocalIdentity&) : _fs(nullptr), _rtc(nullptr), _identity(nullptr), _running(false), _last_status_publish(0), - _token_refresh_count(0), _abandoned_client_count(0), _token_refresh_active_until_ms(0), _last_status{}, _node_name(nullptr), _network(nullptr) { + _token_refresh_count(0), _token_refresh_active_until_ms(0), _last_status{}, _node_name(nullptr), _network(nullptr) { MQTTPrefsStore::setDefaults(_prefs); } @@ -1545,6 +1304,8 @@ bool MQTTUplink::setOwnerPublicKey(const char*) { return false; } bool MQTTUplink::setOwnerEmail(const char*) { return false; } bool MQTTUplink::setCustomHost(const char*) { return false; } bool MQTTUplink::setCustomPort(const char*) { return false; } +bool MQTTUplink::setCustomTransport(const char*) { return false; } +const char* MQTTUplink::getCustomTransport() const { return "tcp"; } bool MQTTUplink::setCustomUsername(const char*) { return false; } bool MQTTUplink::setCustomPassword(const char*) { return false; } bool MQTTUplink::sendStatusNow() { return false; } diff --git a/src/helpers/mqtt/MQTTUplink.h b/src/helpers/mqtt/MQTTUplink.h index 4ae80e2e..2b4e9115 100644 --- a/src/helpers/mqtt/MQTTUplink.h +++ b/src/helpers/mqtt/MQTTUplink.h @@ -65,6 +65,7 @@ public: const char* getOwnerPublicKey() const { return _prefs.owner_public_key; } bool setOwnerEmail(const char* owner_email); const char* getOwnerEmail() const { return _prefs.owner_email; } + // Custom broker (bit 0x08). "-" clears host / username / password. bool setCustomHost(const char* host); const char* getCustomHost() const { return _prefs.custom_host; } bool setCustomPort(const char* port); @@ -91,10 +92,10 @@ private: const char* host; const char* uri; uint8_t bit; - bool custom; - // Per-broker JWT lifetime in seconds; 0 = use the default kTokenLifetimeSecs. waev enforces a - // shorter max token lifetime than the curated default, so it gets its own (matches its config). - uint32_t token_ttl_secs; + bool plain_tcp; + const char* username; + const char* password; + bool custom; // host/port/credentials come from prefs }; struct BrokerState { @@ -135,18 +136,22 @@ private: NetworkStateProvider* _network; #if defined(ESP_PLATFORM) - static constexpr uint8_t kEastmeshBit = 0x01; - static constexpr uint8_t kLetsmeshEuBit = 0x02; - static constexpr uint8_t kLetsmeshUsBit = 0x04; + static constexpr uint8_t kMeshcoretelBit = 0x01; static constexpr uint8_t kCustomBit = 0x08; - static constexpr uint8_t kMeshmapperBit = 0x10; - static constexpr uint8_t kWaevBit = 0x20; - static constexpr uint8_t kBrokerMask = 0x3F; + // Bits 0x02, 0x04, 0x10 and 0x20 are retired. The remaining brokers keep their + // original bit values so already-saved masks stay readable. + static constexpr uint8_t kBrokerMask = kMeshcoretelBit | kCustomBit; static constexpr uint8_t kMaxEnabledBrokers = 2; - static constexpr size_t kBrokerCount = 6; + static constexpr size_t kBrokerCount = 2; static const BrokerSpec kBrokerSpecs[kBrokerCount]; static bool isUnsetIataValue(const char* iata); - static const char* brokerCaCert(const BrokerSpec& spec); + const char* brokerHost(const BrokerState& broker) const; + uint16_t brokerPort(const BrokerState& broker) const; + bool brokerUsesWss(const BrokerState& broker) const; + bool brokerUsesToken(const BrokerState& broker) const; + const char* brokerUri(BrokerState& broker) const; + bool isBrokerConfigured(const BrokerState& broker) const; + void resetCustomBroker(); BrokerState _brokers[kBrokerCount]; @@ -156,13 +161,6 @@ private: uint8_t countEnabledBrokers() const; uint8_t countConnectedBrokers() const; static uint8_t normalizeEnabledMask(uint8_t mask); - bool hasConnectHeadroom(const BrokerState& broker) const; - bool preflightBroker(BrokerState& broker) const; - const char* brokerHost(const BrokerState& broker) const; - uint16_t brokerPort(const BrokerState& broker) const; - bool brokerUsesWss(const BrokerState& broker) const; - const char* brokerUri(BrokerState& broker) const; - bool isBrokerConfigured(const BrokerState& broker) const; void formatTopic(char* dst, size_t dst_size, const char* leaf) const; void refreshIdentityStrings(); void refreshBrokerIdentity(BrokerState& broker); diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index b76a1b6c..f7ebe70b 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -54,12 +54,75 @@ public: // convert UTF-8 characters to displayable block characters for compatibility virtual void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) { size_t j = 0; + #ifdef OLED_RU + char last_char = 0; + char cc = 0; + #endif for (size_t i = 0; src[i] != 0 && j < dest_size - 1; i++) { unsigned char c = (unsigned char)src[i]; if (c >= 32 && c <= 126) { + #ifdef OLED_RU + last_char = 0; + dest[j++] = c; // ASCII printable + } else if (c == 0xC2 || c == 0xC3 || c == 0xD0 || c == 0xD1 || c == 0xD2) { // Cyrillic UTF-8 convert + last_char = c; + c = src[++i]; // Get next char + + if (c != 0) { // Not end of a string + switch (last_char) { + case 0xC3: { + cc = (c | 0xC0); + break; + } + // map UTF-8 cyrillic cars to it Windows-1251 (CP-1251) ASCII codes + case 0xD0: { + if (c == 132) + cc = (170); // Є + if (c == 134) + cc = (178); // І + if (c == 135) + cc = (175); // Ї + if (c == 129) + cc = (168); // Ё + if (c > 143 && c < 192) + cc = (c + 48); + break; + } + case 0xD1: { + if (c == 148) + cc = (186); // є + if (c == 150) + cc = (179); // і + if (c == 151) + cc = (191); // ї + if (c == 145) + cc = (184); // ё + if (c > 127 && c < 144) + cc = (c + 112); + break; + } + case 0xD2: { + if (c == 144) + cc = (165); // Ґ + if (c == 145) + cc = (180); // ґ + break; + } + } + if (cc > 0) { + dest[j++] = cc; + } else { + dest[j++] = '\xAE'; // CP1251 smile emoji replace + } + } + } else if (c >= 0x80) { + last_char = 0; + dest[j++] = '\xAE'; // CP1251 smile emoji replace + #else dest[j++] = c; // ASCII printable } else if (c >= 0x80) { dest[j++] = '\xDB'; // CP437 full block █ + #endif while (src[i+1] && (src[i+1] & 0xC0) == 0x80) i++; // skip UTF-8 continuation bytes } diff --git a/src/helpers/ui/SSD1306Display.cpp b/src/helpers/ui/SSD1306Display.cpp index ab2d77fb..482f0abd 100644 --- a/src/helpers/ui/SSD1306Display.cpp +++ b/src/helpers/ui/SSD1306Display.cpp @@ -1,4 +1,7 @@ #include "SSD1306Display.h" +#ifdef OLED_RU + #include "glcdfont6x8.h" +#endif bool SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) { wire.beginTransmission(addr); @@ -58,13 +61,22 @@ void SSD1306Display::clear() { void SSD1306Display::startFrame(ColorVal bkg) { display.clearDisplay(); // TODO: apply 'bkg' _color = SSD1306_WHITE; + #ifdef OLED_RU + display.setFont(&glcdfont6x8); + #endif display.setTextColor(_color); display.setTextSize(1); display.cp437(true); // Use full 256 char 'Code Page 437' font + #ifdef OLED_RU + display.setCursor(0, 7); + #endif } void SSD1306Display::setTextSize(int sz) { display.setTextSize(sz); + #ifdef OLED_RU + _size = sz; + #endif } void SSD1306Display::setColor(ColorVal c) { @@ -73,7 +85,11 @@ void SSD1306Display::setColor(ColorVal c) { } void SSD1306Display::setCursor(int x, int y) { - display.setCursor(x, y); + #ifdef OLED_RU + display.setCursor(x, y + (_size * 7)); + #else + display.setCursor(x, y); + #endif } void SSD1306Display::print(const char* str) { diff --git a/src/helpers/ui/SSD1306Display.h b/src/helpers/ui/SSD1306Display.h index 5dabbc58..faa60104 100644 --- a/src/helpers/ui/SSD1306Display.h +++ b/src/helpers/ui/SSD1306Display.h @@ -20,6 +20,9 @@ class SSD1306Display : public DisplayDriver { bool _isOn; uint8_t _color; RefCountedDigitalPin* _peripher_power; + #ifdef OLED_RU + uint8_t _size; + #endif bool i2c_probe(TwoWire& wire, uint8_t addr); public: diff --git a/src/helpers/ui/glcdfont6x8.h b/src/helpers/ui/glcdfont6x8.h new file mode 100644 index 00000000..7c634023 --- /dev/null +++ b/src/helpers/ui/glcdfont6x8.h @@ -0,0 +1,460 @@ +// Adafruit GFX font font size: 6x8 + +const uint8_t glcdfont6x8Bitmaps[] PROGMEM = { + 0x00,0x00,0x00,0x00,0x00,0x00, // 32 + 0x20,0x82,0x08,0x20,0x02,0x00, // 33 + 0x51,0x45,0x00,0x00,0x00,0x00, // 34 + 0x51,0x4F,0x94,0xF9,0x45,0x00, // 35 + 0x21,0xEA,0x1C,0x2B,0xC2,0x00, // 36 + 0xC3,0x21,0x08,0x42,0x61,0x80, // 37 + 0x42,0x8A,0x10,0xAA,0x46,0x80, // 38 + 0x20,0x82,0x00,0x00,0x00,0x00, // 39 + 0x10,0x84,0x10,0x40,0x81,0x00, // 40 + 0x40,0x81,0x04,0x10,0x84,0x00, // 41 + 0x00,0x8A,0x9C,0xA8,0x80,0x00, // 42 + 0x00,0x82,0x3E,0x20,0x80,0x00, // 43 + 0x00,0x00,0x00,0x01,0x82,0x10, // 44 + 0x00,0x00,0x3E,0x00,0x00,0x00, // 45 + 0x00,0x00,0x00,0x01,0x86,0x00, // 46 + 0x00,0x21,0x08,0x42,0x00,0x00, // 47 + 0x72,0x29,0xAA,0xCA,0x27,0x00, // 48 + 0x21,0x8A,0x08,0x20,0x8F,0x80, // 49 + 0x72,0x20,0x84,0x21,0x0F,0x80, // 50 + 0xF8,0x42,0x04,0x0A,0x27,0x00, // 51 + 0x10,0xC5,0x24,0xF8,0x41,0x00, // 52 + 0xFA,0x0F,0x02,0x0A,0x27,0x00, // 53 + 0x31,0x08,0x3C,0x8A,0x27,0x00, // 54 + 0xF8,0x21,0x08,0x41,0x04,0x00, // 55 + 0x72,0x28,0x9C,0x8A,0x27,0x00, // 56 + 0x72,0x28,0x9E,0x08,0x46,0x00, // 57 + 0x00,0x06,0x18,0x01,0x86,0x00, // 58 + 0x00,0x06,0x18,0x01,0x82,0x10, // 59 + 0x10,0x84,0x20,0x40,0x81,0x00, // 60 + 0x00,0x0F,0x80,0xF8,0x00,0x00, // 61 + 0x40,0x81,0x02,0x10,0x84,0x00, // 62 + 0x72,0x20,0x84,0x20,0x02,0x00, // 63 + 0x72,0x2B,0xAA,0xBA,0x07,0x80, // 64 + 0x21,0x48,0xA2,0xFA,0x28,0x80, // 65 + 0xF2,0x28,0xBC,0x8A,0x2F,0x00, // 66 + 0x72,0x28,0x20,0x82,0x27,0x00, // 67 + 0xE2,0x48,0xA2,0x8A,0x4E,0x00, // 68 + 0xFA,0x08,0x3C,0x82,0x0F,0x80, // 69 + 0xFA,0x08,0x3C,0x82,0x08,0x00, // 70 + 0x72,0x28,0x2E,0x8A,0x27,0x80, // 71 + 0x8A,0x28,0xBE,0x8A,0x28,0x80, // 72 + 0x70,0x82,0x08,0x20,0x87,0x00, // 73 + 0x38,0x41,0x04,0x12,0x46,0x00, // 74 + 0x8A,0x4A,0x30,0xA2,0x48,0x80, // 75 + 0x82,0x08,0x20,0x82,0x2F,0x80, // 76 + 0x8B,0x6A,0xAA,0x8A,0x28,0x80, // 77 + 0x8A,0x2C,0xAA,0x9A,0x28,0x80, // 78 + 0x72,0x28,0xA2,0x8A,0x27,0x00, // 79 + 0xF2,0x28,0xBC,0x82,0x08,0x00, // 80 + 0x72,0x28,0xA2,0xAA,0x46,0x80, // 81 + 0xF2,0x28,0xBC,0xA2,0x48,0x80, // 82 + 0x7A,0x08,0x1C,0x08,0x2F,0x00, // 83 + 0xFA,0xA2,0x08,0x20,0x82,0x00, // 84 + 0x8A,0x28,0xA2,0x8A,0x27,0x00, // 85 + 0x8A,0x28,0xA2,0x89,0x42,0x00, // 86 + 0x8A,0x2A,0xAA,0xAA,0xA5,0x00, // 87 + 0x8A,0x25,0x08,0x52,0x28,0x80, // 88 + 0x8A,0x28,0x94,0x20,0x82,0x00, // 89 + 0xF8,0x21,0x08,0x42,0x0F,0x80, // 90 + 0x71,0x04,0x10,0x41,0x07,0x00, // 91 + 0x02,0x04,0x08,0x10,0x20,0x00, // 92 + 0x70,0x41,0x04,0x10,0x47,0x00, // 93 + 0x21,0x48,0x80,0x00,0x00,0x00, // 94 + 0x00,0x00,0x00,0x00,0x0F,0x80, // 95 + 0x40,0x81,0x00,0x00,0x00,0x00, // 96 + 0x00,0x07,0x02,0x7A,0x27,0x80, // 97 + 0x82,0x0B,0x32,0x8A,0x2F,0x00, // 98 + 0x00,0x07,0x22,0x82,0x07,0x80, // 99 + 0x08,0x26,0xA6,0x8A,0x27,0x80, // 100 + 0x00,0x07,0x22,0xFA,0x07,0x00, // 101 + 0x31,0x24,0x38,0x41,0x04,0x00, // 102 + 0x00,0x06,0xA6,0x78,0x27,0x00, // 103 + 0x82,0x0B,0x32,0x8A,0x28,0x80, // 104 + 0x20,0x06,0x08,0x20,0x87,0x00, // 105 + 0x10,0x03,0x04,0x12,0x46,0x00, // 106 + 0x82,0x08,0xA4,0xE2,0x48,0x80, // 107 + 0x60,0x82,0x08,0x20,0x87,0x00, // 108 + 0x00,0x0D,0x2A,0xAA,0xAA,0x80, // 109 + 0x00,0x0B,0x32,0x8A,0x28,0x80, // 110 + 0x00,0x07,0x22,0x8A,0x27,0x00, // 111 + 0x00,0x0F,0x22,0xF2,0x08,0x00, // 112 + 0x00,0x07,0xA6,0x68,0x20,0x80, // 113 + 0x00,0x0B,0x32,0x82,0x08,0x00, // 114 + 0x00,0x07,0xA0,0x70,0x2F,0x00, // 115 + 0x41,0x0E,0x10,0x41,0x23,0x00, // 116 + 0x00,0x08,0xA2,0x8A,0x66,0x80, // 117 + 0x00,0x08,0xA2,0x89,0x42,0x00, // 118 + 0x00,0x08,0xAA,0xAA,0xA5,0x00, // 119 + 0x00,0x08,0x94,0x21,0x48,0x80, // 120 + 0x00,0x08,0xA2,0x78,0x27,0x00, // 121 + 0x00,0x0F,0x84,0x21,0x0F,0x80, // 122 + 0x10,0x82,0x10,0x20,0x81,0x00, // 123 + 0x20,0x82,0x08,0x20,0x82,0x00, // 124 + 0x40,0x82,0x04,0x20,0x84,0x00, // 125 + 0x42,0xA1,0x00,0x00,0x00,0x00, // 126 + 0x00,0x02,0x14,0x8A,0x2F,0x80, // 127 + 0x21,0x45,0x14,0x20,0x00,0x00, // 128 + 0x21,0x82,0x08,0x70,0x00,0x00, // 129 + 0x21,0x41,0x08,0x70,0x00,0x00, // 130 + 0x70,0x42,0x04,0x60,0x00,0x00, // 131 + 0x51,0x47,0x04,0x10,0x00,0x00, // 132 + 0x71,0x06,0x04,0x60,0x00,0x00, // 133 + 0x31,0x06,0x14,0x20,0x00,0x00, // 134 + 0x70,0x42,0x08,0x20,0x00,0x00, // 135 + 0x21,0x42,0x14,0x20,0x00,0x00, // 136 + 0x21,0x43,0x04,0x60,0x00,0x00, // 137 + 0x00,0x07,0x00,0x00,0x00,0x00, // 138 + 0xFB,0xEF,0xBE,0xFB,0xEF,0xBE, // 139 + 0x02,0x08,0x20,0x82,0x00,0x00, // 140 + 0x03,0x0C,0x30,0xC3,0x00,0x00, // 141 + 0x03,0x4D,0x34,0xD3,0x40,0x00, // 142 + 0x03,0x6D,0xB6,0xDB,0x60,0x00, // 143 + 0x00,0x00,0x00,0x00,0x00,0x3E, // 144 + 0x00,0x00,0x00,0x00,0x0F,0xBE, // 145 + 0x00,0x00,0x00,0xF8,0x0F,0xBE, // 146 + 0x00,0x00,0x3E,0xF8,0x0F,0xBE, // 147 + 0x03,0xE0,0x3E,0xF8,0x0F,0xBE, // 148 + 0xFB,0xE0,0x3E,0xF8,0x0F,0xBE, // 149 + 0x00,0x00,0x00,0x00,0x0A,0x80, // 150 + 0x00,0x00,0x00,0x02,0x0A,0x80, // 151 + 0x00,0x00,0x00,0x22,0x8A,0x80, // 152 + 0x00,0x00,0x02,0x2A,0xAA,0x80, // 153 + 0x00,0x08,0x20,0x82,0x0A,0x80, // 154 + 0x00,0x8A,0x28,0xA2,0x8A,0x80, // 155 + 0x08,0xAA,0xAA,0xAA,0xAA,0x80, // 156 + 0x79,0x0C,0x20,0xC1,0x07,0x80, // 157 + 0xF8,0x00,0x00,0x00,0x0F,0x80, // 158 + 0xF0,0x41,0x04,0x10,0x4F,0x00, // 159 + 0x79,0x0D,0xA6,0xD9,0x07,0x80, // 160 + 0xF8,0x01,0x86,0x18,0x0F,0x80, // 161 + 0xF8,0x0D,0xB6,0xD8,0x0F,0x80, // 162 + 0xF0,0x4D,0x34,0xD0,0x4F,0x00, // 163 + 0x02,0x27,0x14,0x72,0x20,0x00, // 164 + 0x13,0xE8,0x20,0x82,0x08,0x00, // 165 + 0x20,0x82,0x00,0x20,0x82,0x00, // 166 + 0x7A,0x0F,0x14,0x78,0x2F,0x00, // 167 + 0x50,0x0F,0xA0,0xF2,0x0F,0x80, // 168 + 0x00,0x80,0x3E,0x00,0x80,0x00, // 169 + 0x72,0x28,0x38,0x82,0x27,0x00, // 170 + 0x00,0x84,0xA4,0x48,0x80,0x00, // 171 + 0xFA,0x28,0xA2,0xBA,0xCE,0x00, // 172 + 0xFA,0xA7,0x08,0x20,0x82,0x00, // 173 + 0x01,0xCA,0xBE,0x8B,0x67,0x00, // 174 + 0x50,0x07,0x08,0x20,0x87,0x00, // 175 + 0x62,0x49,0x18,0x00,0x00,0x00, // 176 + 0x20,0x8F,0x88,0x20,0x0F,0x80, // 177 + 0x70,0x82,0x08,0x20,0x87,0x00, // 178 + 0x20,0x06,0x08,0x20,0x87,0x00, // 179 + 0x00,0x01,0x3E,0x8A,0x08,0x00, // 180 + 0x00,0x05,0x14,0x51,0xC4,0xA0, // 181 + 0x00,0x0F,0x94,0x51,0x49,0x80, // 182 + 0x00,0x00,0x08,0x00,0x00,0x00, // 183 + 0x50,0x07,0x22,0xFA,0x07,0x00, // 184 + 0x1A,0x49,0x34,0xB2,0x49,0x00, // 185 + 0x00,0x07,0x22,0x3A,0x27,0x00, // 186 + 0x00,0x89,0x12,0x90,0x80,0x00, // 187 + 0x03,0x8C,0x28,0x10,0x20,0x00, // 188 + 0x00,0xE1,0x8A,0x42,0x00,0x00, // 189 + 0x00,0x21,0x28,0xC3,0x80,0x00, // 190 + 0x50,0x06,0x08,0x20,0x87,0x00, // 191 + 0x72,0x28,0xA2,0xFA,0x28,0x80, // 192 + 0xFA,0x08,0x3C,0x8A,0x2F,0x00, // 193 + 0xF2,0x28,0xBC,0x8A,0x2F,0x00, // 194 + 0xFA,0x28,0x20,0x82,0x08,0x00, // 195 + 0x71,0x45,0x14,0x51,0x4F,0xA2, // 196 + 0xFA,0x08,0x3C,0x82,0x0F,0x80, // 197 + 0xAA,0xAA,0x9C,0xAA,0xAA,0x80, // 198 + 0xF0,0x20,0x9C,0x08,0x2F,0x00, // 199 + 0x8A,0x29,0xAA,0xCA,0x28,0x80, // 200 + 0x50,0x88,0xA6,0xAB,0x28,0x80, // 201 + 0x8A,0x4A,0x30,0xA2,0x48,0x80, // 202 + 0x39,0x24,0x92,0x49,0x28,0x80, // 203 + 0x8B,0x6A,0xAA,0x8A,0x28,0x80, // 204 + 0x8A,0x28,0xBE,0x8A,0x28,0x80, // 205 + 0x72,0x28,0xA2,0x8A,0x27,0x00, // 206 + 0xFA,0x28,0xA2,0x8A,0x28,0x80, // 207 + 0xF2,0x28,0xBC,0x82,0x08,0x00, // 208 + 0x72,0x28,0x20,0x82,0x27,0x00, // 209 + 0xF8,0x82,0x08,0x20,0x82,0x00, // 210 + 0x8A,0x28,0x9E,0x08,0x27,0x00, // 211 + 0x72,0xAA,0xAA,0x70,0x82,0x00, // 212 + 0x8A,0x25,0x08,0x52,0x28,0x80, // 213 + 0x92,0x49,0x24,0x92,0x4F,0x82, // 214 + 0x8A,0x28,0x9E,0x08,0x20,0x80, // 215 + 0xAA,0xAA,0xAA,0xAA,0xAF,0x80, // 216 + 0xAA,0xAA,0xAA,0xAA,0xAF,0x82, // 217 + 0xC1,0x04,0x1C,0x49,0x27,0x00, // 218 + 0x8A,0x28,0xBA,0x9A,0x6E,0x80, // 219 + 0x82,0x08,0x3C,0x8A,0x2F,0x00, // 220 + 0x72,0x20,0x8E,0x0A,0x27,0x00, // 221 + 0x92,0xAA,0xBA,0xAA,0xA9,0x00, // 222 + 0x7A,0x28,0x9E,0x29,0x28,0x80, // 223 + 0x00,0x07,0x02,0x7A,0x27,0x80, // 224 + 0x19,0x88,0x3C,0x8A,0x27,0x00, // 225 + 0x00,0x0F,0x22,0xF2,0x2F,0x00, // 226 + 0x00,0x0F,0xA2,0x82,0x08,0x00, // 227 + 0x00,0x03,0x14,0x51,0x4F,0xA2, // 228 + 0x00,0x07,0x22,0xFA,0x07,0x00, // 229 + 0x00,0x0A,0xAA,0x72,0xAA,0x80, // 230 + 0x00,0x0F,0x02,0x70,0x2F,0x00, // 231 + 0x00,0x08,0xA6,0xAB,0x28,0x80, // 232 + 0x00,0x8A,0xA2,0x9A,0xAC,0x80, // 233 + 0x00,0x08,0xA4,0xE2,0x48,0x80, // 234 + 0x00,0x03,0x92,0x49,0x28,0x80, // 235 + 0x00,0x08,0xB6,0xAA,0x28,0x80, // 236 + 0x00,0x08,0xA2,0xFA,0x28,0x80, // 237 + 0x00,0x07,0x22,0x8A,0x27,0x00, // 238 + 0x00,0x0F,0xA2,0x8A,0x28,0x80, // 239 + 0x00,0x0F,0x22,0xF2,0x08,0x00, // 240 + 0x00,0x07,0x22,0x82,0x07,0x80, // 241 + 0x00,0x0F,0x88,0x20,0x82,0x00, // 242 + 0x00,0x08,0xA2,0x78,0x27,0x00, // 243 + 0x00,0x07,0x2A,0xA9,0xC2,0x08, // 244 + 0x00,0x08,0x94,0x21,0x48,0x80, // 245 + 0x00,0x09,0x24,0x92,0x4F,0x82, // 246 + 0x00,0x08,0xA2,0x78,0x20,0x80, // 247 + 0x00,0x0A,0xAA,0xAA,0xAF,0x80, // 248 + 0x00,0x0A,0xAA,0xAA,0xAF,0x82, // 249 + 0x00,0x0C,0x10,0x71,0x27,0x00, // 250 + 0x00,0x08,0xA2,0xEA,0x6E,0x80, // 251 + 0x00,0x08,0x20,0xF2,0x2F,0x00, // 252 + 0x00,0x07,0x22,0x3A,0x27,0x00, // 253 + 0x00,0x09,0x2A,0xEA,0xA9,0x00, // 254 + 0x00,0x07,0xA2,0x79,0x28,0x80 // 255 +}; + +const GFXglyph glcdfont6x8Glyphs[] PROGMEM = { + { 0, 6, 8, 6, 0, -7 }, + { 6, 6, 8, 6, 0, -7 }, + { 12, 6, 8, 6, 0, -7 }, + { 18, 6, 8, 6, 0, -7 }, + { 24, 6, 8, 6, 0, -7 }, + { 30, 6, 8, 6, 0, -7 }, + { 36, 6, 8, 6, 0, -7 }, + { 42, 6, 8, 6, 0, -7 }, + { 48, 6, 8, 6, 0, -7 }, + { 54, 6, 8, 6, 0, -7 }, + { 60, 6, 8, 6, 0, -7 }, + { 66, 6, 8, 6, 0, -7 }, + { 72, 6, 8, 6, 0, -7 }, + { 78, 6, 8, 6, 0, -7 }, + { 84, 6, 8, 6, 0, -7 }, + { 90, 6, 8, 6, 0, -7 }, + { 96, 6, 8, 6, 0, -7 }, + { 102, 6, 8, 6, 0, -7 }, + { 108, 6, 8, 6, 0, -7 }, + { 114, 6, 8, 6, 0, -7 }, + { 120, 6, 8, 6, 0, -7 }, + { 126, 6, 8, 6, 0, -7 }, + { 132, 6, 8, 6, 0, -7 }, + { 138, 6, 8, 6, 0, -7 }, + { 144, 6, 8, 6, 0, -7 }, + { 150, 6, 8, 6, 0, -7 }, + { 156, 6, 8, 6, 0, -7 }, + { 162, 6, 8, 6, 0, -7 }, + { 168, 6, 8, 6, 0, -7 }, + { 174, 6, 8, 6, 0, -7 }, + { 180, 6, 8, 6, 0, -7 }, + { 186, 6, 8, 6, 0, -7 }, + { 192, 6, 8, 6, 0, -7 }, + { 198, 6, 8, 6, 0, -7 }, + { 204, 6, 8, 6, 0, -7 }, + { 210, 6, 8, 6, 0, -7 }, + { 216, 6, 8, 6, 0, -7 }, + { 222, 6, 8, 6, 0, -7 }, + { 228, 6, 8, 6, 0, -7 }, + { 234, 6, 8, 6, 0, -7 }, + { 240, 6, 8, 6, 0, -7 }, + { 246, 6, 8, 6, 0, -7 }, + { 252, 6, 8, 6, 0, -7 }, + { 258, 6, 8, 6, 0, -7 }, + { 264, 6, 8, 6, 0, -7 }, + { 270, 6, 8, 6, 0, -7 }, + { 276, 6, 8, 6, 0, -7 }, + { 282, 6, 8, 6, 0, -7 }, + { 288, 6, 8, 6, 0, -7 }, + { 294, 6, 8, 6, 0, -7 }, + { 300, 6, 8, 6, 0, -7 }, + { 306, 6, 8, 6, 0, -7 }, + { 312, 6, 8, 6, 0, -7 }, + { 318, 6, 8, 6, 0, -7 }, + { 324, 6, 8, 6, 0, -7 }, + { 330, 6, 8, 6, 0, -7 }, + { 336, 6, 8, 6, 0, -7 }, + { 342, 6, 8, 6, 0, -7 }, + { 348, 6, 8, 6, 0, -7 }, + { 354, 6, 8, 6, 0, -7 }, + { 360, 6, 8, 6, 0, -7 }, + { 366, 6, 8, 6, 0, -7 }, + { 372, 6, 8, 6, 0, -7 }, + { 378, 6, 8, 6, 0, -7 }, + { 384, 6, 8, 6, 0, -7 }, + { 390, 6, 8, 6, 0, -7 }, + { 396, 6, 8, 6, 0, -7 }, + { 402, 6, 8, 6, 0, -7 }, + { 408, 6, 8, 6, 0, -7 }, + { 414, 6, 8, 6, 0, -7 }, + { 420, 6, 8, 6, 0, -7 }, + { 426, 6, 8, 6, 0, -7 }, + { 432, 6, 8, 6, 0, -7 }, + { 438, 6, 8, 6, 0, -7 }, + { 444, 6, 8, 6, 0, -7 }, + { 450, 6, 8, 6, 0, -7 }, + { 456, 6, 8, 6, 0, -7 }, + { 462, 6, 8, 6, 0, -7 }, + { 468, 6, 8, 6, 0, -7 }, + { 474, 6, 8, 6, 0, -7 }, + { 480, 6, 8, 6, 0, -7 }, + { 486, 6, 8, 6, 0, -7 }, + { 492, 6, 8, 6, 0, -7 }, + { 498, 6, 8, 6, 0, -7 }, + { 504, 6, 8, 6, 0, -7 }, + { 510, 6, 8, 6, 0, -7 }, + { 516, 6, 8, 6, 0, -7 }, + { 522, 6, 8, 6, 0, -7 }, + { 528, 6, 8, 6, 0, -7 }, + { 534, 6, 8, 6, 0, -7 }, + { 540, 6, 8, 6, 0, -7 }, + { 546, 6, 8, 6, 0, -7 }, + { 552, 6, 8, 6, 0, -7 }, + { 558, 6, 8, 6, 0, -7 }, + { 564, 6, 8, 6, 0, -7 }, + { 570, 6, 8, 6, 0, -7 }, + { 576, 6, 8, 6, 0, -7 }, + { 582, 6, 8, 6, 0, -7 }, + { 588, 6, 8, 6, 0, -7 }, + { 594, 6, 8, 6, 0, -7 }, + { 600, 6, 8, 6, 0, -7 }, + { 606, 6, 8, 6, 0, -7 }, + { 612, 6, 8, 6, 0, -7 }, + { 618, 6, 8, 6, 0, -7 }, + { 624, 6, 8, 6, 0, -7 }, + { 630, 6, 8, 6, 0, -7 }, + { 636, 6, 8, 6, 0, -7 }, + { 642, 6, 8, 6, 0, -7 }, + { 648, 6, 8, 6, 0, -7 }, + { 654, 6, 8, 6, 0, -7 }, + { 660, 6, 8, 6, 0, -7 }, + { 666, 6, 8, 6, 0, -7 }, + { 672, 6, 8, 6, 0, -7 }, + { 678, 6, 8, 6, 0, -7 }, + { 684, 6, 8, 6, 0, -7 }, + { 690, 6, 8, 6, 0, -7 }, + { 696, 6, 8, 6, 0, -7 }, + { 702, 6, 8, 6, 0, -7 }, + { 708, 6, 8, 6, 0, -7 }, + { 714, 6, 8, 6, 0, -7 }, + { 720, 6, 8, 6, 0, -7 }, + { 726, 6, 8, 6, 0, -7 }, + { 732, 6, 8, 6, 0, -7 }, + { 738, 6, 8, 6, 0, -7 }, + { 744, 6, 8, 6, 0, -7 }, + { 750, 6, 8, 6, 0, -7 }, + { 756, 6, 8, 6, 0, -7 }, + { 762, 6, 8, 6, 0, -7 }, + { 768, 6, 8, 6, 0, -7 }, + { 774, 6, 8, 6, 0, -7 }, + { 780, 6, 8, 6, 0, -7 }, + { 786, 6, 8, 6, 0, -7 }, + { 792, 6, 8, 6, 0, -7 }, + { 798, 6, 8, 6, 0, -7 }, + { 804, 6, 8, 6, 0, -7 }, + { 810, 6, 8, 6, 0, -7 }, + { 816, 6, 8, 6, 0, -7 }, + { 822, 6, 8, 6, 0, -7 }, + { 828, 6, 8, 6, 0, -7 }, + { 834, 6, 8, 6, 0, -7 }, + { 840, 6, 8, 6, 0, -7 }, + { 846, 6, 8, 6, 0, -7 }, + { 852, 6, 8, 6, 0, -7 }, + { 858, 6, 8, 6, 0, -7 }, + { 864, 6, 8, 6, 0, -7 }, + { 870, 6, 8, 6, 0, -7 }, + { 876, 6, 8, 6, 0, -7 }, + { 882, 6, 8, 6, 0, -7 }, + { 888, 6, 8, 6, 0, -7 }, + { 894, 6, 8, 6, 0, -7 }, + { 900, 6, 8, 6, 0, -7 }, + { 906, 6, 8, 6, 0, -7 }, + { 912, 6, 8, 6, 0, -7 }, + { 918, 6, 8, 6, 0, -7 }, + { 924, 6, 8, 6, 0, -7 }, + { 930, 6, 8, 6, 0, -7 }, + { 936, 6, 8, 6, 0, -7 }, + { 942, 6, 8, 6, 0, -7 }, + { 948, 6, 8, 6, 0, -7 }, + { 954, 6, 8, 6, 0, -7 }, + { 960, 6, 8, 6, 0, -7 }, + { 966, 6, 8, 6, 0, -7 }, + { 972, 6, 8, 6, 0, -7 }, + { 978, 6, 8, 6, 0, -7 }, + { 984, 6, 8, 6, 0, -7 }, + { 990, 6, 8, 6, 0, -7 }, + { 996, 6, 8, 6, 0, -7 }, + { 1002, 6, 8, 6, 0, -7 }, + { 1008, 6, 8, 6, 0, -7 }, + { 1014, 6, 8, 6, 0, -7 }, + { 1020, 6, 8, 6, 0, -7 }, + { 1026, 6, 8, 6, 0, -7 }, + { 1032, 6, 8, 6, 0, -7 }, + { 1038, 6, 8, 6, 0, -7 }, + { 1044, 6, 8, 6, 0, -7 }, + { 1050, 6, 8, 6, 0, -7 }, + { 1056, 6, 8, 6, 0, -7 }, + { 1062, 6, 8, 6, 0, -7 }, + { 1068, 6, 8, 6, 0, -7 }, + { 1074, 6, 8, 6, 0, -7 }, + { 1080, 6, 8, 6, 0, -7 }, + { 1086, 6, 8, 6, 0, -7 }, + { 1092, 6, 8, 6, 0, -7 }, + { 1098, 6, 8, 6, 0, -7 }, + { 1104, 6, 8, 6, 0, -7 }, + { 1110, 6, 8, 6, 0, -7 }, + { 1116, 6, 8, 6, 0, -7 }, + { 1122, 6, 8, 6, 0, -7 }, + { 1128, 6, 8, 6, 0, -7 }, + { 1134, 6, 8, 6, 0, -7 }, + { 1140, 6, 8, 6, 0, -7 }, + { 1146, 6, 8, 6, 0, -7 }, + { 1152, 6, 8, 6, 0, -7 }, + { 1158, 6, 8, 6, 0, -7 }, + { 1164, 6, 8, 6, 0, -7 }, + { 1170, 6, 8, 6, 0, -7 }, + { 1176, 6, 8, 6, 0, -7 }, + { 1182, 6, 8, 6, 0, -7 }, + { 1188, 6, 8, 6, 0, -7 }, + { 1194, 6, 8, 6, 0, -7 }, + { 1200, 6, 8, 6, 0, -7 }, + { 1206, 6, 8, 6, 0, -7 }, + { 1212, 6, 8, 6, 0, -7 }, + { 1218, 6, 8, 6, 0, -7 }, + { 1224, 6, 8, 6, 0, -7 }, + { 1230, 6, 8, 6, 0, -7 }, + { 1236, 6, 8, 6, 0, -7 }, + { 1242, 6, 8, 6, 0, -7 }, + { 1248, 6, 8, 6, 0, -7 }, + { 1254, 6, 8, 6, 0, -7 }, + { 1260, 6, 8, 6, 0, -7 }, + { 1266, 6, 8, 6, 0, -7 }, + { 1272, 6, 8, 6, 0, -7 }, + { 1278, 6, 8, 6, 0, -7 }, + { 1284, 6, 8, 6, 0, -7 }, + { 1290, 6, 8, 6, 0, -7 }, + { 1296, 6, 8, 6, 0, -7 }, + { 1302, 6, 8, 6, 0, -7 }, + { 1308, 6, 8, 6, 0, -7 }, + { 1314, 6, 8, 6, 0, -7 }, + { 1320, 6, 8, 6, 0, -7 }, + { 1326, 6, 8, 6, 0, -7 }, + { 1332, 6, 8, 6, 0, -7 }, + { 1338, 6, 8, 6, 0, -7 } +}; + +const GFXfont glcdfont6x8 PROGMEM = { + (uint8_t *)glcdfont6x8Bitmaps, + (GFXglyph *)glcdfont6x8Glyphs, + 32, 255, 8}; diff --git a/src/helpers/web/EastMeshFavicon.h b/src/helpers/web/EastMeshFavicon.h deleted file mode 100644 index 14d79492..00000000 --- a/src/helpers/web/EastMeshFavicon.h +++ /dev/null @@ -1,153 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace eastmesh_web_assets { - -static const uint8_t kFaviconPng[] PROGMEM = { - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, - 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, - 0x08, 0x06, 0x00, 0x00, 0x00, 0x73, 0x7a, 0x7a, 0xf4, 0x00, 0x00, 0x06, - 0x49, 0x49, 0x44, 0x41, 0x54, 0x58, 0x09, 0xed, 0x56, 0x6d, 0x6c, 0x5b, - 0xe5, 0x15, 0x7e, 0xde, 0xfb, 0xe5, 0x7b, 0x6d, 0xe7, 0x3a, 0xf6, 0x92, - 0x3a, 0x75, 0x92, 0xb6, 0x4e, 0x48, 0x13, 0x68, 0x52, 0xda, 0x44, 0x59, - 0x35, 0x8d, 0x74, 0xed, 0xc6, 0x36, 0xc8, 0x8f, 0x6a, 0x20, 0xa5, 0x62, - 0x4c, 0x93, 0x56, 0x8a, 0x8a, 0xd8, 0xc6, 0xa6, 0x49, 0xe3, 0x17, 0x12, - 0xa4, 0x9b, 0xb6, 0x49, 0x9b, 0x3a, 0x34, 0x89, 0xb1, 0xad, 0x9a, 0xa6, - 0x21, 0x10, 0xd3, 0xd0, 0x3a, 0x21, 0x26, 0x15, 0x52, 0x15, 0xd1, 0xb5, - 0x5d, 0x81, 0x36, 0x81, 0x02, 0x6d, 0x49, 0x4a, 0xd2, 0xa6, 0x4e, 0x13, - 0xdb, 0x71, 0xfc, 0x79, 0x7d, 0xaf, 0xaf, 0xef, 0xd7, 0xbb, 0x37, 0x45, - 0x9d, 0x08, 0xb5, 0x4b, 0x24, 0x7e, 0x4d, 0xea, 0xf9, 0x61, 0xeb, 0x9e, - 0xf7, 0xbd, 0xe7, 0x3c, 0xe7, 0x9c, 0xe7, 0x9c, 0x73, 0x81, 0x5b, 0xf2, - 0xff, 0x9c, 0x81, 0xbe, 0x2f, 0x0d, 0xf7, 0x51, 0x80, 0x7c, 0x9e, 0x18, - 0xf8, 0xcf, 0xf1, 0x32, 0x17, 0x8e, 0x76, 0x1e, 0x7a, 0xb2, 0x7b, 0xc8, - 0x32, 0xfb, 0x76, 0xba, 0xb8, 0x6b, 0xbb, 0x89, 0x77, 0x4e, 0xda, 0xd8, - 0xa8, 0x0e, 0xe3, 0x9f, 0x47, 0xa6, 0x57, 0x6b, 0x97, 0x5b, 0xcd, 0xc5, - 0xd7, 0x16, 0xe3, 0x43, 0x63, 0xa9, 0xf5, 0xf1, 0xff, 0x45, 0x3b, 0xb0, - 0x4f, 0xc4, 0xd0, 0xbe, 0x68, 0xff, 0x60, 0x57, 0xab, 0x17, 0x89, 0x3e, - 0x0f, 0x9e, 0xbc, 0x0e, 0xdd, 0x6a, 0x03, 0xf6, 0x13, 0xf0, 0xd2, 0xa3, - 0x18, 0xfd, 0x49, 0x78, 0x35, 0x76, 0x97, 0xef, 0x7c, 0x26, 0x80, 0xbf, - 0x2d, 0x76, 0xef, 0x35, 0xf8, 0xe0, 0x9e, 0x25, 0xf4, 0x3f, 0xbc, 0xd7, - 0x7c, 0x36, 0x8e, 0xc1, 0x1f, 0xdd, 0x03, 0x45, 0x3d, 0x0d, 0xb7, 0xe1, - 0x70, 0xbc, 0x6b, 0xeb, 0x06, 0x39, 0x14, 0x01, 0x84, 0x80, 0x03, 0x51, - 0xdd, 0x02, 0x32, 0x0a, 0x54, 0xbc, 0x28, 0x9a, 0xdb, 0x7e, 0x87, 0x7d, - 0x0c, 0xe4, 0x2a, 0x44, 0xb8, 0xd9, 0x9d, 0xfd, 0x0b, 0x5f, 0xbc, 0x2f, - 0x49, 0x85, 0x6d, 0x5c, 0xb5, 0xb7, 0x78, 0x28, 0xf7, 0x15, 0xf7, 0xe4, - 0x73, 0xee, 0x51, 0xb2, 0x21, 0xda, 0x82, 0x79, 0x4d, 0x91, 0x24, 0x09, - 0xc9, 0x85, 0x34, 0x14, 0x51, 0x01, 0x09, 0xa8, 0xad, 0x54, 0x72, 0x5f, - 0xe4, 0x9f, 0x78, 0xfa, 0xa1, 0xbb, 0x9b, 0xe3, 0x9d, 0x6f, 0xe6, 0xb3, - 0x83, 0x25, 0x35, 0xfa, 0x2c, 0xb3, 0xfd, 0xe6, 0xcd, 0xec, 0x2f, 0x9f, - 0xd5, 0xe5, 0xc0, 0xc8, 0xdc, 0x37, 0x23, 0x45, 0xe0, 0xc7, 0x8b, 0x76, - 0x7f, 0xfa, 0x1c, 0xed, 0x49, 0x1f, 0xfd, 0x87, 0xf2, 0x08, 0x29, 0x71, - 0x1b, 0x68, 0x30, 0x22, 0x2a, 0xbe, 0x00, 0xd6, 0xfb, 0x25, 0x5c, 0x9c, - 0x38, 0x45, 0xbb, 0xef, 0xd8, 0x04, 0x3e, 0x1c, 0x22, 0xad, 0xed, 0x21, - 0xec, 0xba, 0x73, 0x63, 0x47, 0xab, 0xd2, 0xa0, 0xec, 0x6c, 0x6d, 0xc7, - 0xa9, 0xac, 0xd1, 0xeb, 0xde, 0xfe, 0xe5, 0x13, 0x78, 0xfb, 0x68, 0xf6, - 0x66, 0x20, 0xea, 0x02, 0x90, 0x7e, 0xd8, 0x7f, 0x5f, 0xc6, 0x8d, 0xf8, - 0xa7, 0xad, 0x0e, 0xdf, 0x99, 0xa9, 0x2d, 0x1e, 0xce, 0x25, 0xbf, 0x05, - 0x37, 0x00, 0x41, 0x0c, 0x23, 0x98, 0xd7, 0x31, 0xbc, 0xbd, 0x8f, 0x6e, - 0x54, 0x2b, 0x18, 0xb9, 0x77, 0x08, 0x43, 0x3d, 0x9d, 0xd8, 0xa0, 0xf8, - 0x61, 0xf3, 0x1e, 0xfc, 0xbc, 0x08, 0xbd, 0xec, 0x91, 0xa6, 0x68, 0x4b, - 0xdb, 0x64, 0xa6, 0xfc, 0x0d, 0xc8, 0x9d, 0x2f, 0x62, 0xe6, 0xad, 0x4a, - 0x3d, 0x10, 0x75, 0x39, 0x30, 0xef, 0xb4, 0x6d, 0xba, 0x54, 0xec, 0x29, - 0x5c, 0xba, 0xa4, 0x0a, 0xde, 0x85, 0xcc, 0x00, 0x72, 0x16, 0x68, 0x99, - 0x07, 0x9f, 0xab, 0x22, 0xa6, 0x86, 0x68, 0xef, 0x17, 0x38, 0x10, 0xd7, - 0x44, 0x2c, 0xe0, 0xe2, 0xf6, 0x35, 0x01, 0x88, 0x10, 0x10, 0xb6, 0x24, - 0x54, 0x2a, 0x1e, 0x92, 0x59, 0x0b, 0x83, 0x3e, 0x3f, 0x6b, 0x50, 0x75, - 0x23, 0xda, 0xbb, 0x9f, 0xc7, 0x32, 0x69, 0xeb, 0x48, 0x5d, 0x00, 0x85, - 0x54, 0x73, 0x48, 0x2b, 0xb6, 0xea, 0xde, 0x6b, 0xa9, 0x07, 0x30, 0x3e, - 0x3b, 0x4c, 0xe7, 0x4b, 0xc0, 0x62, 0x15, 0x44, 0x73, 0x58, 0x94, 0x40, - 0x48, 0xe6, 0x70, 0x35, 0x55, 0x80, 0x6d, 0x39, 0xc4, 0xf3, 0x28, 0x44, - 0x8e, 0x23, 0xa6, 0xe1, 0xa0, 0x52, 0xa4, 0xa0, 0x16, 0xa1, 0x82, 0x49, - 0xa9, 0x90, 0x31, 0x01, 0x5b, 0x1e, 0x86, 0xd4, 0xf6, 0x02, 0x5a, 0xf6, - 0x34, 0xd7, 0xc2, 0x50, 0x97, 0x84, 0x76, 0x22, 0x52, 0x01, 0x2f, 0x0b, - 0xd0, 0xe4, 0x2a, 0x66, 0xb3, 0x02, 0x2c, 0xd6, 0x84, 0x7a, 0x1a, 0x1e, - 0xcf, 0x21, 0xef, 0xf8, 0x51, 0x32, 0x62, 0x78, 0xe0, 0xfe, 0x9d, 0x24, - 0xd8, 0xd8, 0x04, 0xc3, 0x22, 0xe4, 0xf2, 0x7c, 0x1e, 0x32, 0x0f, 0x22, - 0x5b, 0x1e, 0x0a, 0x29, 0x06, 0x36, 0x20, 0xc2, 0x9b, 0xcc, 0x00, 0xa6, - 0x05, 0x18, 0xce, 0x6e, 0xc8, 0xc2, 0xaf, 0x18, 0x00, 0xa6, 0x58, 0x29, - 0x75, 0x33, 0x80, 0xab, 0xf2, 0x59, 0x24, 0xbc, 0x1e, 0xd0, 0xd8, 0x18, - 0x72, 0x69, 0x66, 0xc8, 0x01, 0xd2, 0x1f, 0xc2, 0xca, 0xcc, 0xc2, 0x2a, - 0x66, 0xf1, 0x9f, 0xf3, 0x8b, 0xd0, 0x2a, 0x0e, 0x8e, 0x8c, 0xcf, 0xe1, - 0x8f, 0xaf, 0x9c, 0x85, 0xa9, 0x9b, 0x34, 0x35, 0x9f, 0x45, 0x98, 0xf1, - 0xc0, 0x4a, 0x17, 0x68, 0x26, 0xab, 0xc3, 0xcb, 0xe6, 0x99, 0xcb, 0xab, - 0x40, 0x7e, 0x9e, 0xb5, 0x67, 0x76, 0xdd, 0x4a, 0xd7, 0x1f, 0x3f, 0xd5, - 0xcd, 0x00, 0x16, 0x9d, 0x57, 0xa1, 0xfb, 0xbf, 0x8f, 0x4b, 0x89, 0x71, - 0x70, 0x41, 0x97, 0x14, 0x12, 0x3c, 0x25, 0xac, 0xee, 0x02, 0x8f, 0x8c, - 0x25, 0x90, 0x33, 0x97, 0x73, 0x8c, 0x78, 0x65, 0x9c, 0xb8, 0x52, 0xc2, - 0xa9, 0x8f, 0x0a, 0xf0, 0x11, 0x97, 0xdc, 0xbb, 0xa5, 0x15, 0x93, 0x1f, - 0x26, 0xf0, 0xe0, 0xae, 0x41, 0xfc, 0xfa, 0x85, 0xd3, 0x40, 0x39, 0x0f, - 0x62, 0x97, 0x41, 0x0d, 0xd6, 0x08, 0x66, 0x81, 0x35, 0xd5, 0x8d, 0x52, - 0xb7, 0x0b, 0xf0, 0xd6, 0x2b, 0x15, 0xc4, 0xbe, 0x4b, 0x90, 0x2b, 0xfd, - 0x02, 0xa9, 0xf7, 0x39, 0x50, 0x96, 0x01, 0x8e, 0x5d, 0x57, 0xd4, 0x82, - 0x4d, 0x84, 0x3f, 0x1b, 0x46, 0xe9, 0x2f, 0x57, 0xde, 0x39, 0xbe, 0x2d, - 0x9b, 0xd7, 0x0f, 0x68, 0x2c, 0x50, 0x47, 0x37, 0xda, 0x27, 0xa7, 0xd2, - 0x4e, 0x2c, 0xd6, 0xc8, 0x5f, 0x4c, 0x14, 0xc9, 0x7b, 0xe3, 0x17, 0x21, - 0x78, 0x1a, 0x88, 0x59, 0x02, 0x35, 0x0b, 0xa7, 0xa1, 0xea, 0xbf, 0x84, - 0x96, 0xb4, 0x3f, 0x0d, 0xe1, 0x33, 0x16, 0x09, 0x25, 0xd8, 0xf6, 0xf7, - 0x67, 0x90, 0x39, 0xf3, 0x35, 0x14, 0xdf, 0x9d, 0x82, 0x12, 0x79, 0x1a, - 0x72, 0x64, 0x0e, 0xd3, 0x07, 0x67, 0xe8, 0x28, 0x84, 0xde, 0xc3, 0xbb, - 0x66, 0xda, 0xc2, 0xde, 0xc8, 0xd8, 0xfc, 0x96, 0x2c, 0x5c, 0xf3, 0x7e, - 0x78, 0x64, 0x2f, 0x08, 0xdf, 0x8d, 0x8a, 0xc6, 0xc8, 0x57, 0x04, 0x71, - 0x74, 0xd0, 0x6a, 0x39, 0xcd, 0xce, 0xbe, 0x8a, 0xf2, 0x89, 0x0b, 0x9f, - 0x76, 0xbe, 0xfc, 0x5c, 0xbf, 0x04, 0xd7, 0x6e, 0x13, 0x8a, 0xb7, 0xf1, - 0x18, 0x76, 0xbc, 0xe4, 0xc7, 0xb1, 0x03, 0xe5, 0x4f, 0x1a, 0x78, 0xbc, - 0xfc, 0x53, 0xdf, 0x43, 0xdf, 0x59, 0xef, 0x2c, 0x2d, 0xe5, 0x7d, 0x63, - 0x3f, 0x7f, 0x72, 0x06, 0x03, 0xa3, 0xbf, 0x87, 0x6e, 0x9e, 0x41, 0xd5, - 0x78, 0x03, 0xb6, 0x01, 0xb8, 0x36, 0x28, 0x65, 0x14, 0xe3, 0xf0, 0x1c, - 0x8a, 0xb5, 0x9d, 0xaf, 0x02, 0xc0, 0x35, 0x97, 0x1e, 0x8e, 0xed, 0x5e, - 0xe1, 0x7c, 0x59, 0x1b, 0x0c, 0x06, 0xa9, 0xc2, 0xa6, 0xa1, 0x70, 0xbd, - 0xc3, 0x27, 0x46, 0x99, 0x57, 0xfa, 0x6f, 0xc4, 0x1f, 0x7b, 0x04, 0xbc, - 0xd8, 0x0f, 0x97, 0x88, 0xa0, 0xde, 0x39, 0x96, 0x95, 0x97, 0xaf, 0x59, - 0xa9, 0xf3, 0x53, 0xbf, 0x0b, 0xea, 0xbc, 0x70, 0x5d, 0xfd, 0xd4, 0xda, - 0x7f, 0xd9, 0xc9, 0xe4, 0x12, 0x75, 0x2a, 0x25, 0xd6, 0x67, 0xd7, 0x85, - 0x65, 0xec, 0xf2, 0x33, 0x07, 0xd1, 0xb3, 0xed, 0x67, 0xe8, 0x18, 0xa4, - 0xe8, 0xd9, 0x71, 0x08, 0xc5, 0x23, 0x97, 0xaf, 0x9f, 0xd6, 0xfa, 0xbf, - 0x59, 0x09, 0xc8, 0xc0, 0xf8, 0x80, 0xb0, 0xc6, 0x11, 0x14, 0xcf, 0x92, - 0x02, 0xbe, 0x80, 0x3f, 0xb0, 0x46, 0x0a, 0xfa, 0x73, 0x95, 0x2e, 0xdf, - 0xc9, 0x99, 0x4d, 0x62, 0xe3, 0xe9, 0xbc, 0x5f, 0x39, 0x7e, 0x4c, 0x41, - 0x7b, 0x57, 0x3f, 0x9e, 0x38, 0x4c, 0x10, 0x69, 0xae, 0x22, 0xaf, 0x19, - 0xa8, 0x06, 0x8b, 0x51, 0xcd, 0x76, 0xb2, 0x17, 0xc6, 0xa7, 0x83, 0x9c, - 0x57, 0x2a, 0xd4, 0xf2, 0xfa, 0x09, 0x5d, 0x2d, 0x12, 0x92, 0xcd, 0xbf, - 0xd9, 0xec, 0x0f, 0xb5, 0x88, 0x2a, 0x09, 0x88, 0x8d, 0xae, 0x4c, 0x43, - 0x02, 0xa1, 0x44, 0x14, 0xfc, 0x3e, 0x85, 0x6f, 0x88, 0xe9, 0x66, 0x24, - 0x74, 0xfc, 0x00, 0xf7, 0xa8, 0x57, 0x74, 0xfa, 0x3c, 0x83, 0x91, 0x5a, - 0x0c, 0xb2, 0x7a, 0xa8, 0xac, 0xd6, 0xa2, 0x17, 0x8b, 0x86, 0xac, 0xa6, - 0x06, 0x35, 0x37, 0x9d, 0x70, 0x5e, 0x5e, 0x1f, 0x6b, 0xf9, 0xc3, 0x42, - 0x32, 0x69, 0x28, 0x62, 0x45, 0xe3, 0x16, 0x67, 0xf5, 0x85, 0x89, 0x83, - 0xcb, 0xfb, 0x80, 0x4d, 0xb3, 0x95, 0x52, 0x2b, 0x03, 0x54, 0x54, 0x45, - 0xab, 0xe0, 0x6f, 0x7a, 0x98, 0x97, 0xa8, 0xe7, 0x2c, 0x4f, 0x58, 0x97, - 0x17, 0xa9, 0xc1, 0x96, 0xbe, 0x03, 0xaa, 0xa5, 0xd2, 0xc1, 0x1f, 0xdc, - 0xb5, 0x9b, 0xc4, 0x3b, 0xd6, 0xb2, 0x21, 0xe7, 0xa1, 0x62, 0x53, 0xf8, - 0x44, 0x0e, 0x9e, 0x0b, 0xce, 0xb2, 0x3d, 0xb9, 0xaf, 0xbb, 0x35, 0xf6, - 0xea, 0xc9, 0xa9, 0x81, 0xc7, 0xbf, 0x7d, 0x67, 0x32, 0x1c, 0xfe, 0x7a, - 0x57, 0x56, 0x2b, 0xcb, 0x56, 0x39, 0x67, 0x6f, 0xdd, 0x7a, 0x90, 0x71, - 0xe4, 0x46, 0xa9, 0x05, 0x00, 0x66, 0xb2, 0x83, 0x2c, 0x19, 0x74, 0x1d, - 0x27, 0xb9, 0x69, 0x5b, 0xe0, 0xd6, 0x39, 0x1e, 0xe1, 0xa8, 0x23, 0x78, - 0xd4, 0xc5, 0x64, 0x69, 0x4e, 0xcc, 0x3a, 0x92, 0xc1, 0x5d, 0x9c, 0x99, - 0x83, 0xc0, 0xc6, 0x72, 0x4f, 0xbc, 0x05, 0xe9, 0xa5, 0x02, 0x63, 0x3c, - 0x41, 0x53, 0x44, 0xc5, 0xd8, 0xf1, 0x0f, 0x10, 0x0a, 0x48, 0xb2, 0xa6, - 0x5b, 0x7b, 0x7c, 0xbe, 0xd2, 0x76, 0x91, 0x4d, 0x12, 0x5e, 0x09, 0xfd, - 0x96, 0xb9, 0xae, 0x59, 0x8d, 0x9a, 0x00, 0xce, 0x1f, 0xdb, 0xe4, 0xa1, - 0x57, 0x79, 0x4a, 0x8c, 0x77, 0x46, 0xa9, 0x79, 0x25, 0x42, 0xab, 0xa9, - 0x08, 0x27, 0x2a, 0x2a, 0x4f, 0x54, 0x4b, 0x14, 0x9c, 0xc6, 0x3f, 0x4d, - 0x7c, 0x90, 0x80, 0xee, 0xb2, 0xf5, 0xc8, 0xc1, 0xf5, 0x38, 0xde, 0xef, - 0x57, 0x18, 0x3a, 0xe2, 0xb3, 0x2b, 0xa6, 0x4c, 0x75, 0xdd, 0x27, 0xf9, - 0x42, 0xef, 0x9f, 0xf8, 0x48, 0x3f, 0x2f, 0x87, 0x23, 0x6f, 0xdc, 0xd6, - 0xb5, 0xb6, 0xa4, 0x4f, 0x25, 0xae, 0xde, 0x18, 0xfb, 0xc7, 0x9a, 0x9a, - 0x1c, 0xc0, 0x8e, 0xef, 0x85, 0x70, 0xdb, 0x40, 0x18, 0x7d, 0x9b, 0x05, - 0x64, 0xde, 0xf3, 0xa0, 0xcd, 0xd8, 0x08, 0x04, 0xaa, 0xa8, 0x4a, 0x2e, - 0xb2, 0x70, 0x30, 0x3c, 0x62, 0x60, 0xe4, 0x0e, 0x17, 0xfb, 0xd9, 0x17, - 0xf1, 0xda, 0x09, 0x82, 0x73, 0x21, 0x0e, 0x3e, 0x43, 0x80, 0x5e, 0x92, - 0xb0, 0x94, 0xf4, 0xb1, 0xd9, 0xcf, 0xb6, 0x95, 0xd6, 0x80, 0x58, 0xa7, - 0x80, 0xe6, 0x66, 0x17, 0xe7, 0xcf, 0x6a, 0x58, 0xb8, 0x30, 0x8f, 0xd9, - 0xbf, 0xb2, 0xf5, 0x78, 0x4b, 0x6e, 0x65, 0xe0, 0x56, 0x06, 0x56, 0x66, - 0xe0, 0xbf, 0x94, 0xca, 0xb0, 0x29, 0x61, 0x87, 0x57, 0xda, 0x00, 0x00, - 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 -}; - -static constexpr size_t kFaviconPngLen = sizeof(kFaviconPng); - -} // namespace eastmesh_web_assets diff --git a/src/helpers/web/WebCertStore.cpp b/src/helpers/web/WebCertStore.cpp new file mode 100644 index 00000000..8621f96f --- /dev/null +++ b/src/helpers/web/WebCertStore.cpp @@ -0,0 +1,467 @@ +#include "WebCertStore.h" + +#include +#include +#include + +#if defined(ESP_PLATFORM) + #include + #include + #include + #include + #include + #include + #include "../mqtt/generated/WebPanelCert.h" +#endif + +namespace { +constexpr const char* kCertPath = "/web_cert.pem"; +constexpr const char* kKeyPath = "/web_key.pem"; +constexpr const char* kBeginMarker = "-----BEGIN "; +constexpr const char* kEndMarker = "-----END "; + +void setErr(char* err, size_t err_size, const char* text) { + if (err != nullptr && err_size > 0) { + snprintf(err, err_size, "%s", text); + } +} + +// Copy the value of the first CN= attribute out of an mbedtls DN string ("CN=foo, O=bar"). +void extractCn(const char* dn, char* out, size_t out_size) { + if (out_size == 0) { + return; + } + out[0] = 0; + const char* p = strstr(dn, "CN="); + if (p == nullptr) { + snprintf(out, out_size, "%s", dn); + return; + } + p += 3; + size_t i = 0; + while (p[i] != 0 && p[i] != ',' && i + 1 < out_size) { + out[i] = p[i]; + i++; + } + out[i] = 0; +} +} // namespace + +WebCertStore::WebCertStore() + : _fs(nullptr), _cert(nullptr), _cert_len(0), _key(nullptr), _key_len(0), _input(nullptr), _input_len(0), + _old_cert(nullptr), _old_key(nullptr) {} + +WebCertStore::~WebCertStore() { + freeBuffer(_cert); + freeBuffer(_key); + freeBuffer(_input); + releaseRetired(); +} + +char* WebCertStore::allocBuffer(size_t size) { +#if defined(ESP_PLATFORM) + void* ptr = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (ptr == nullptr) { + ptr = heap_caps_malloc(size, MALLOC_CAP_8BIT); + } + return static_cast(ptr); +#else + return static_cast(malloc(size)); +#endif +} + +void WebCertStore::freeBuffer(void* ptr) { + if (ptr != nullptr) { + free(ptr); + } +} + +void WebCertStore::begin(FILESYSTEM* fs) { + _fs = fs; +#if defined(ESP_PLATFORM) + if (_fs == nullptr || !_fs->exists(kCertPath) || !_fs->exists(kKeyPath)) { + return; + } + char* cert = nullptr; + char* key = nullptr; + size_t cert_len = 0; + size_t key_len = 0; + if (!loadFile(kCertPath, &cert, &cert_len, kMaxCertPem) || !loadFile(kKeyPath, &key, &key_len, kMaxKeyPem)) { + Serial.println("[WEB] custom certificate files unreadable, using built-in certificate"); + freeBuffer(cert); + freeBuffer(key); + return; + } + char err[64]; + if (!validatePair(cert, cert_len, key, key_len, err, sizeof(err))) { + Serial.printf("[WEB] custom certificate rejected (%s), using built-in certificate\n", err); + freeBuffer(cert); + freeBuffer(key); + return; + } + adopt(cert, cert_len, key, key_len); + Serial.println("[WEB] custom TLS certificate loaded"); +#endif +} + +void WebCertStore::releaseRetired() { + freeBuffer(_old_cert); + freeBuffer(_old_key); + _old_cert = nullptr; + _old_key = nullptr; +} + +void WebCertStore::adopt(char* cert, size_t cert_len, char* key, size_t key_len) { + // The running HTTPS server may still reference the previous buffers: keep them until it is stopped. + releaseRetired(); + _old_cert = _cert; + _old_key = _key; + _cert = cert; + _cert_len = cert_len; + _key = key; + _key_len = key_len; +} + +bool WebCertStore::loadFile(const char* path, char** out, size_t* out_len, size_t max_len) { +#if defined(ESP_PLATFORM) + File file = _fs->open(path); + if (!file) { + return false; + } + size_t size = static_cast(file.size()); + if (size == 0 || size > max_len) { + file.close(); + return false; + } + char* buf = allocBuffer(size + 1); + if (buf == nullptr) { + file.close(); + return false; + } + size_t got = file.read(reinterpret_cast(buf), size); + file.close(); + if (got != size) { + freeBuffer(buf); + return false; + } + buf[size] = 0; + *out = buf; + *out_len = size + 1; // esp-tls PEM length includes the NUL + return true; +#else + (void)path; (void)out; (void)out_len; (void)max_len; + return false; +#endif +} + +bool WebCertStore::saveFile(const char* path, const char* data, size_t len) { +#if defined(ESP_PLATFORM) + if (_fs == nullptr) { + return false; + } + if (_fs->exists(path) && !_fs->remove(path)) { + return false; + } + File file = _fs->open(path, "w", true); + if (!file) { + return false; + } + bool ok = file.write(reinterpret_cast(data), len) == len; + file.close(); + return ok; +#else + (void)path; (void)data; (void)len; + return false; +#endif +} + +// Split a PEM bundle into the certificate chain (in input order) and the single private key. +bool WebCertStore::splitBundle(const char* bundle, size_t len, char* cert_out, size_t cert_cap, size_t* cert_len, + char* key_out, size_t key_cap, size_t* key_len, char* err, size_t err_size) { + *cert_len = 0; + *key_len = 0; + cert_out[0] = 0; + key_out[0] = 0; + const char* end = bundle + len; + const char* p = bundle; + int certs = 0; + int keys = 0; + while (p < end) { + const char* begin = strstr(p, kBeginMarker); + if (begin == nullptr || begin >= end) { + break; + } + const char* label = begin + strlen(kBeginMarker); + const char* label_end = strstr(label, "-----"); + if (label_end == nullptr) { + setErr(err, err_size, "malformed PEM header"); + return false; + } + const char* block_end = strstr(label_end, kEndMarker); + if (block_end == nullptr) { + setErr(err, err_size, "missing PEM footer"); + return false; + } + const char* footer_end = strstr(block_end + strlen(kEndMarker), "-----"); + if (footer_end == nullptr) { + setErr(err, err_size, "malformed PEM footer"); + return false; + } + footer_end += 5; + size_t block_len = static_cast(footer_end - begin); + size_t label_len = static_cast(label_end - label); + bool is_cert = label_len == 11 && memcmp(label, "CERTIFICATE", 11) == 0; + bool is_key = strstr(label, "PRIVATE KEY") != nullptr && strstr(label, "PRIVATE KEY") < label_end; + bool is_encrypted = strstr(label, "ENCRYPTED") != nullptr && strstr(label, "ENCRYPTED") < label_end; + if (is_cert) { + if (*cert_len + block_len + 1 >= cert_cap) { + setErr(err, err_size, "certificate chain too large"); + return false; + } + memcpy(cert_out + *cert_len, begin, block_len); + *cert_len += block_len; + cert_out[(*cert_len)++] = '\n'; + cert_out[*cert_len] = 0; + certs++; + } else if (is_key) { + if (is_encrypted) { + setErr(err, err_size, "encrypted private keys are not supported"); + return false; + } + if (keys > 0) { + setErr(err, err_size, "more than one private key"); + return false; + } + if (block_len + 2 >= key_cap) { + setErr(err, err_size, "private key too large"); + return false; + } + memcpy(key_out, begin, block_len); + *key_len = block_len; + key_out[(*key_len)++] = '\n'; + key_out[*key_len] = 0; + keys++; + } + // Other blocks (EC PARAMETERS, comments) are ignored. + p = footer_end; + } + if (certs == 0) { + setErr(err, err_size, "no certificate found"); + return false; + } + if (keys == 0) { + setErr(err, err_size, "no private key found"); + return false; + } + return true; +} + +bool WebCertStore::validatePair(const char* cert_pem, size_t cert_len, const char* key_pem, size_t key_len, char* err, + size_t err_size) { +#if defined(ESP_PLATFORM) + // mbedtls wants PEM buffers NUL-terminated with the NUL counted in the length. + size_t cert_n = cert_pem[cert_len - 1] == 0 ? cert_len : strlen(cert_pem) + 1; + size_t key_n = key_pem[key_len - 1] == 0 ? key_len : strlen(key_pem) + 1; + + mbedtls_x509_crt crt; + mbedtls_x509_crt_init(&crt); + int rc = mbedtls_x509_crt_parse(&crt, reinterpret_cast(cert_pem), cert_n); + if (rc != 0) { + mbedtls_x509_crt_free(&crt); + char text[64]; + snprintf(text, sizeof(text), "certificate parse failed (-0x%04x)", static_cast(-rc)); + setErr(err, err_size, text); + return false; + } + + mbedtls_pk_context pk; + mbedtls_pk_init(&pk); +#if MBEDTLS_VERSION_MAJOR >= 3 + rc = mbedtls_pk_parse_key(&pk, reinterpret_cast(key_pem), key_n, nullptr, 0, + mbedtls_ctr_drbg_random, nullptr); +#else + rc = mbedtls_pk_parse_key(&pk, reinterpret_cast(key_pem), key_n, nullptr, 0); +#endif + if (rc != 0) { + mbedtls_pk_free(&pk); + mbedtls_x509_crt_free(&crt); + char text[64]; + snprintf(text, sizeof(text), "private key parse failed (-0x%04x)", static_cast(-rc)); + setErr(err, err_size, text); + return false; + } + bool ok = true; + if (mbedtls_pk_get_type(&pk) != MBEDTLS_PK_RSA) { + setErr(err, err_size, "RSA key required (server uses RSA cipher suites only)"); + ok = false; + } else if (mbedtls_pk_get_bitlen(&pk) < 2048) { + setErr(err, err_size, "RSA key must be at least 2048 bits"); + ok = false; + } else { +#if MBEDTLS_VERSION_MAJOR >= 3 + rc = mbedtls_pk_check_pair(&crt.pk, &pk, mbedtls_ctr_drbg_random, nullptr); +#else + rc = mbedtls_pk_check_pair(&crt.pk, &pk); +#endif + if (rc != 0) { + setErr(err, err_size, "private key does not match the certificate"); + ok = false; + } + } + mbedtls_pk_free(&pk); + mbedtls_x509_crt_free(&crt); + return ok; +#else + (void)cert_pem; (void)cert_len; (void)key_pem; (void)key_len; + setErr(err, err_size, "unsupported platform"); + return false; +#endif +} + +bool WebCertStore::install(const char* bundle, size_t len, char* err, size_t err_size) { + if (bundle == nullptr || len == 0) { + setErr(err, err_size, "empty input"); + return false; + } + if (len > kMaxBundle) { + setErr(err, err_size, "bundle too large"); + return false; + } + if (_fs == nullptr) { + setErr(err, err_size, "no filesystem"); + return false; + } + char* cert = allocBuffer(kMaxCertPem + 1); + char* key = allocBuffer(kMaxKeyPem + 1); + if (cert == nullptr || key == nullptr) { + freeBuffer(cert); + freeBuffer(key); + setErr(err, err_size, "out of memory"); + return false; + } + size_t cert_len = 0; + size_t key_len = 0; + if (!splitBundle(bundle, len, cert, kMaxCertPem + 1, &cert_len, key, kMaxKeyPem + 1, &key_len, err, err_size) || + !validatePair(cert, cert_len + 1, key, key_len + 1, err, err_size)) { + freeBuffer(cert); + freeBuffer(key); + return false; + } + if (!saveFile(kCertPath, cert, cert_len) || !saveFile(kKeyPath, key, key_len)) { + freeBuffer(cert); + freeBuffer(key); + setErr(err, err_size, "write to filesystem failed"); + return false; + } + adopt(cert, cert_len + 1, key, key_len + 1); + return true; +} + +bool WebCertStore::reset() { + bool ok = true; +#if defined(ESP_PLATFORM) + if (_fs != nullptr) { + if (_fs->exists(kCertPath)) { + ok = _fs->remove(kCertPath) && ok; + } + if (_fs->exists(kKeyPath)) { + ok = _fs->remove(kKeyPath) && ok; + } + } +#endif + adopt(nullptr, 0, nullptr, 0); + return ok; +} + +bool WebCertStore::describe(const char* cert_pem, size_t cert_len, Info& info) { + memset(&info, 0, sizeof(info)); +#if defined(ESP_PLATFORM) + size_t cert_n = cert_pem[cert_len - 1] == 0 ? cert_len : strlen(cert_pem) + 1; + mbedtls_x509_crt crt; + mbedtls_x509_crt_init(&crt); + if (mbedtls_x509_crt_parse(&crt, reinterpret_cast(cert_pem), cert_n) != 0) { + mbedtls_x509_crt_free(&crt); + return false; + } + char dn[128]; + if (mbedtls_x509_dn_gets(dn, sizeof(dn), &crt.subject) > 0) { + extractCn(dn, info.subject_cn, sizeof(info.subject_cn)); + } + if (mbedtls_x509_dn_gets(dn, sizeof(dn), &crt.issuer) > 0) { + extractCn(dn, info.issuer_cn, sizeof(info.issuer_cn)); + } + snprintf(info.valid_to, sizeof(info.valid_to), "%04d-%02d-%02d", crt.valid_to.year, crt.valid_to.mon, + crt.valid_to.day); + const char* type = mbedtls_pk_get_type(&crt.pk) == MBEDTLS_PK_RSA ? "RSA" : "EC"; + snprintf(info.key_type, sizeof(info.key_type), "%s%u", type, static_cast(mbedtls_pk_get_bitlen(&crt.pk))); + unsigned char digest[32]; +#if MBEDTLS_VERSION_MAJOR >= 3 + if (mbedtls_sha256(crt.raw.p, crt.raw.len, digest, 0) == 0) { +#else + if (mbedtls_sha256_ret(crt.raw.p, crt.raw.len, digest, 0) == 0) { +#endif + for (int i = 0; i < 8; ++i) { + snprintf(&info.fingerprint[i * 2], 3, "%02X", digest[i]); + } + } + mbedtls_x509_crt_free(&crt); + info.valid = true; + return true; +#else + (void)cert_pem; (void)cert_len; + return false; +#endif +} + +bool WebCertStore::describeActive(Info& info) const { + if (hasCustom()) { + return describe(_cert, _cert_len, info); + } +#if defined(ESP_PLATFORM) + return describe(mqtt_web_panel_cert::kServerCertPem, sizeof(mqtt_web_panel_cert::kServerCertPem), info); +#else + memset(&info, 0, sizeof(info)); + return false; +#endif +} + +bool WebCertStore::inputBegin() { + freeBuffer(_input); + _input = allocBuffer(kMaxBundle + 1); + _input_len = 0; + if (_input != nullptr) { + _input[0] = 0; + } + return _input != nullptr; +} + +bool WebCertStore::inputAdd(const char* line) { + if (_input == nullptr || line == nullptr) { + return false; + } + size_t n = strlen(line); + if (_input_len + n + 2 > kMaxBundle) { + return false; + } + memcpy(_input + _input_len, line, n); + _input_len += n; + _input[_input_len++] = '\n'; + _input[_input_len] = 0; + return true; +} + +bool WebCertStore::inputEnd(char* err, size_t err_size) { + if (_input == nullptr) { + setErr(err, err_size, "no pending input, use 'web cert begin' first"); + return false; + } + bool ok = install(_input, _input_len, err, err_size); + inputAbort(); + return ok; +} + +void WebCertStore::inputAbort() { + freeBuffer(_input); + _input = nullptr; + _input_len = 0; +} diff --git a/src/helpers/web/WebCertStore.h b/src/helpers/web/WebCertStore.h new file mode 100644 index 00000000..e3b38fd8 --- /dev/null +++ b/src/helpers/web/WebCertStore.h @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include + +// Custom TLS certificate for the HTTPS web panel. +// +// The firmware ships a self-signed RSA certificate generated at build time. This store lets the +// operator replace it with their own certificate (or chain) and RSA private key, kept as PEM files +// on the internal filesystem: +// /web_cert.pem - leaf certificate first, optional intermediates after it (max 8 KB) +// /web_key.pem - unencrypted RSA private key, PKCS#1 or PKCS#8 (max 4 KB) +// Input is a single PEM "bundle" (certificate(s) + key in any order), as produced by +// `cat cert.pem key.pem`. Elliptic-curve keys are rejected: the HTTPS server is restricted to RSA +// cipher suites (see arch/esp32/tls_cipher_restrict.c). +class WebCertStore { +public: + static constexpr size_t kMaxCertPem = 8192; + static constexpr size_t kMaxKeyPem = 4096; + static constexpr size_t kMaxBundle = kMaxCertPem + kMaxKeyPem; + + struct Info { + bool valid; + char subject_cn[48]; + char issuer_cn[48]; + char valid_to[11]; // YYYY-MM-DD + char key_type[12]; // e.g. "RSA2048" + char fingerprint[17]; // first 8 bytes of the SHA-256 over the DER certificate, hex + }; + + WebCertStore(); + ~WebCertStore(); + + void begin(FILESYSTEM* fs); + + // Active custom material (nullptr when the built-in certificate is in use). Lengths include the + // terminating NUL, as esp-tls expects for PEM buffers. + const char* certPem() const { return _cert; } + size_t certLen() const { return _cert_len; } + const char* keyPem() const { return _key; } + size_t keyLen() const { return _key_len; } + bool hasCustom() const { return _cert != nullptr && _key != nullptr; } + + // Validate a PEM bundle and, if it is good, persist it and make it active. On failure `err` + // receives a short reason. Never touches the active material on failure. + bool install(const char* bundle, size_t len, char* err, size_t err_size); + // Remove the custom files and fall back to the built-in certificate. + bool reset(); + + // Describe a PEM certificate (chain): the leaf is the first certificate. + static bool describe(const char* cert_pem, size_t cert_len, Info& info); + // Describe whatever is active: custom material or the built-in certificate. + bool describeActive(Info& info) const; + + // Line-by-line input for the CLI: begin(), add() each PEM line, end() installs. + bool inputBegin(); + bool inputAdd(const char* line); + bool inputEnd(char* err, size_t err_size); + void inputAbort(); + // Free material retired by install()/reset(); call once the HTTPS server has been stopped. + void releaseRetired(); + +private: + static char* allocBuffer(size_t size); + static void freeBuffer(void* ptr); + bool loadFile(const char* path, char** out, size_t* out_len, size_t max_len); + bool saveFile(const char* path, const char* data, size_t len); + static bool splitBundle(const char* bundle, size_t len, char* cert_out, size_t cert_cap, size_t* cert_len, + char* key_out, size_t key_cap, size_t* key_len, char* err, size_t err_size); + static bool validatePair(const char* cert_pem, size_t cert_len, const char* key_pem, size_t key_len, char* err, + size_t err_size); + void adopt(char* cert, size_t cert_len, char* key, size_t key_len); + + FILESYSTEM* _fs; + char* _cert; + size_t _cert_len; + char* _key; + size_t _key_len; + char* _input; + size_t _input_len; + char* _old_cert; + char* _old_key; +}; diff --git a/src/helpers/web/WebPanelServer.cpp b/src/helpers/web/WebPanelServer.cpp index 8036c899..6d94056a 100644 --- a/src/helpers/web/WebPanelServer.cpp +++ b/src/helpers/web/WebPanelServer.cpp @@ -10,8 +10,6 @@ #include #include #include - -#include "EastMeshFavicon.h" #include "../mqtt/generated/WebPanelCert.h" namespace { @@ -30,8 +28,8 @@ constexpr size_t kWebCommandBufferSize = 192; constexpr size_t kWebReplyBufferSize = 256; constexpr size_t kWebStatsQueryBufferSize = 96; constexpr size_t kWebStatsReplyBufferSize = 4608; -constexpr size_t kWebPageChunkSize = 768; -constexpr size_t kWebFirmwareChunkSize = 4096; +constexpr size_t kWebCertBundleMax = 12288; +constexpr size_t kWebPageChunkSize = MBEDTLS_SSL_OUT_CONTENT_LEN; constexpr unsigned long kWebIdleTimeoutMs = WEB_PANEL_IDLE_TIMEOUT_MS; #if defined(MQTT_DEBUG) && MQTT_DEBUG @@ -57,7 +55,7 @@ void freeScratchBuffer(void* ptr) { // Internal (DMA-capable) heap headroom needed before we start an HTTPS listener. // A single mbedTLS session needs ~40KB of internal RAM in one piece; starting the // server when that cannot be satisfied just leaves a listener that resets every -// handshake. Mirrors the dual-broker gate in MQTTUplink. +// handshake. constexpr size_t kWebMinFreeHeap = 56U * 1024U; constexpr size_t kWebMinLargestHeap = 32U * 1024U; // Below this largest-block size a TLS handshake can no longer be satisfied; the @@ -81,11 +79,6 @@ esp_err_t finishAndClose(httpd_req_t* req, esp_err_t rc) { return rc; } -void rebootAfterFirmwareUpdateTask(void*) { - vTaskDelay(pdMS_TO_TICKS(1200)); - esp_restart(); -} - void bytesToHexUpper(const uint8_t* src, size_t len, char* dst, size_t dst_size) { if (dst_size == 0) { return; @@ -102,21 +95,28 @@ esp_err_t sendChunk(httpd_req_t* req, const char* text) { return httpd_resp_sendstr_chunk(req, text != nullptr ? text : ""); } -esp_err_t sendProgmemChunked(httpd_req_t* req, const char* text) { - if (text == nullptr) { +#define sendProgmem(req, mem) sendWhole(req, mem, sizeof(mem) - 1) + +esp_err_t sendWhole(httpd_req_t* req, const char* text, const size_t len) { + if (text == nullptr || len == 0) { return httpd_resp_send_chunk(req, nullptr, 0); } - const size_t len = strlen(text); - size_t offset = 0; - while (offset < len) { - const size_t chunk_len = ((len - offset) > kWebPageChunkSize) ? kWebPageChunkSize : (len - offset); - if (httpd_resp_send_chunk(req, &text[offset], chunk_len) != ESP_OK) { - httpd_resp_send_chunk(req, nullptr, 0); + const size_t last_size = ((len - 1) % kWebPageChunkSize) + 1; + const char* last = text + (len - last_size); + + for (const char* p = text; p < last; p += kWebPageChunkSize) { + if (httpd_resp_send_chunk(req, p, kWebPageChunkSize) != ESP_OK) { return ESP_FAIL; } - offset += chunk_len; + + vTaskDelay(1); } + + if (httpd_resp_send_chunk(req, last, last_size) != ESP_OK) { + return ESP_FAIL; + } + return httpd_resp_send_chunk(req, nullptr, 0); } @@ -245,7 +245,6 @@ const char kWebPanelLoginHtml[] PROGMEM = R"HTML( Repeater Login -
-

Repeater Config

+
+

Repeater Config

+ +

Use the repeater admin password to unlock the panel.

@@ -293,12 +297,27 @@ const char kWebPanelLoginHtml[] PROGMEM = R"HTML(
@@ -3326,8 +3346,9 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( } // namespace WebPanelServer::WebPanelServer() - : _runner(nullptr), _server(nullptr), _redirect_server(nullptr), _token{0}, _last_activity_ms(0), - _start_deferred_count(0), _restart_count(0), _route_context{this} { + : _runner(nullptr), _server(nullptr), _redirect_server(nullptr), _token{0}, _last_activity_ms(0), _route_context{this}, _ota_started(false), + _start_deferred_count(0), _restart_count(0), _custom_cert(nullptr), _custom_cert_len(0), _custom_key(nullptr), + _custom_key_len(0) { } void WebPanelServer::setCommandRunner(WebPanelCommandRunner* runner) { @@ -3349,30 +3370,40 @@ bool WebPanelServer::start() { return false; } + // A session kept across a Wi-Fi outage still has to honour the idle timeout. + if (_token[0] != 0 && _last_activity_ms != 0 && kWebIdleTimeoutMs != 0 && + millis() - _last_activity_ms >= kWebIdleTimeoutMs) { + lockSession(); + } + noteActivity(); httpd_ssl_config_t config = HTTPD_SSL_CONFIG_DEFAULT(); config.httpd.max_open_sockets = 2; config.httpd.max_uri_handlers = 9; config.httpd.max_resp_headers = 4; - config.httpd.backlog_conn = 2; - config.httpd.recv_wait_timeout = 15; - config.httpd.send_wait_timeout = 15; - // Evict the least-recently-used connection when the pool is full. Without this, - // clients that vanish without closing (lid shut, out of range) permanently occupy - // the 2 sockets and every later connect is reset (Firefox PR_CONNECT_RESET_ERROR). + config.httpd.backlog_conn = 0; + config.httpd.recv_wait_timeout = 10; + config.httpd.send_wait_timeout = 10; config.httpd.lru_purge_enable = true; config.httpd.stack_size = kWebServerStackSize; + config.httpd.task_priority = tskIDLE_PRIORITY + 2; + config.httpd.core_id = 0; + const bool custom_cert = _custom_cert != nullptr && _custom_key != nullptr; + const uint8_t* cert_pem = reinterpret_cast(custom_cert ? _custom_cert : mqtt_web_panel_cert::kServerCertPem); + const size_t cert_len = custom_cert ? _custom_cert_len : sizeof(mqtt_web_panel_cert::kServerCertPem); + const uint8_t* key_pem = reinterpret_cast(custom_cert ? _custom_key : mqtt_web_panel_cert::kServerKeyPem); + const size_t key_len = custom_cert ? _custom_key_len : sizeof(mqtt_web_panel_cert::kServerKeyPem); #if defined(ESP_IDF_VERSION_MAJOR) && ESP_IDF_VERSION_MAJOR >= 5 - config.servercert = reinterpret_cast(mqtt_web_panel_cert::kServerCertPem); - config.servercert_len = sizeof(mqtt_web_panel_cert::kServerCertPem); + config.servercert = cert_pem; + config.servercert_len = cert_len; #else // IDF 4.x uses the misnamed CA slot for the server certificate. - config.cacert_pem = reinterpret_cast(mqtt_web_panel_cert::kServerCertPem); - config.cacert_len = sizeof(mqtt_web_panel_cert::kServerCertPem); + config.cacert_pem = cert_pem; + config.cacert_len = cert_len; #endif - config.prvtkey_pem = reinterpret_cast(mqtt_web_panel_cert::kServerKeyPem); - config.prvtkey_len = sizeof(mqtt_web_panel_cert::kServerKeyPem); + config.prvtkey_pem = key_pem; + config.prvtkey_len = key_len; esp_err_t rc = httpd_ssl_start(&_server, &config); if (rc != ESP_OK) { @@ -3382,23 +3413,23 @@ bool WebPanelServer::start() { } httpd_uri_t index_uri = {.uri = "/", .method = HTTP_GET, .handler = &WebPanelServer::handleIndex, .user_ctx = &_route_context}; - httpd_uri_t favicon_uri = {.uri = "/favicon.ico", .method = HTTP_GET, .handler = &WebPanelServer::handleFavicon, .user_ctx = &_route_context}; httpd_uri_t app_uri = {.uri = "/app", .method = HTTP_GET, .handler = &WebPanelServer::handleApp, .user_ctx = &_route_context}; httpd_uri_t stats_page_uri = {.uri = "/stats", .method = HTTP_GET, .handler = &WebPanelServer::handleStatsPage, .user_ctx = &_route_context}; httpd_uri_t login_uri = {.uri = "/login", .method = HTTP_POST, .handler = &WebPanelServer::handleLogin, .user_ctx = &_route_context}; - httpd_uri_t session_uri = {.uri = "/api/session", .method = HTTP_GET, .handler = &WebPanelServer::handleSession, .user_ctx = &_route_context}; httpd_uri_t command_uri = {.uri = "/api/command", .method = HTTP_POST, .handler = &WebPanelServer::handleCommand, .user_ctx = &_route_context}; - httpd_uri_t firmware_update_uri = {.uri = "/api/firmware-update", .method = HTTP_POST, .handler = &WebPanelServer::handleFirmwareUpdate, .user_ctx = &_route_context}; httpd_uri_t stats_uri = {.uri = "/api/stats", .method = HTTP_GET, .handler = &WebPanelServer::handleStats, .user_ctx = &_route_context}; + httpd_uri_t update = {.uri = "/update", .method = HTTP_GET, .handler = &WebPanelServer::handleOtaRedirect, .user_ctx = &_route_context}; + httpd_uri_t ota_uri = { .uri = "/api/ota", .method = HTTP_POST, .handler = &WebPanelServer::handleOtaUpload, .user_ctx = &_route_context }; + httpd_uri_t cert_uri = { .uri = "/api/cert", .method = HTTP_POST, .handler = &WebPanelServer::handleCertUpload, .user_ctx = &_route_context }; httpd_register_uri_handler(_server, &index_uri); - httpd_register_uri_handler(_server, &favicon_uri); httpd_register_uri_handler(_server, &app_uri); httpd_register_uri_handler(_server, &stats_page_uri); httpd_register_uri_handler(_server, &login_uri); - httpd_register_uri_handler(_server, &session_uri); httpd_register_uri_handler(_server, &command_uri); - httpd_register_uri_handler(_server, &firmware_update_uri); httpd_register_uri_handler(_server, &stats_uri); + httpd_register_uri_handler(_server, &update); + httpd_register_uri_handler(_server, &ota_uri); + httpd_register_uri_handler(_server, &cert_uri); httpd_config_t redirect_config = HTTPD_DEFAULT_CONFIG(); redirect_config.server_port = 80; @@ -3408,11 +3439,13 @@ bool WebPanelServer::start() { redirect_config.max_open_sockets = 2; redirect_config.max_uri_handlers = 1; redirect_config.max_resp_headers = 4; - redirect_config.backlog_conn = 2; - redirect_config.recv_wait_timeout = 2; - redirect_config.send_wait_timeout = 2; - redirect_config.lru_purge_enable = true; // same zombie-socket protection as the HTTPS listener + redirect_config.backlog_conn = 0; + redirect_config.recv_wait_timeout = 10; + redirect_config.send_wait_timeout = 10; + redirect_config.lru_purge_enable = true; redirect_config.stack_size = kWebServerStackSize; + redirect_config.task_priority = tskIDLE_PRIORITY + 2; + redirect_config.core_id = 0; redirect_config.uri_match_fn = httpd_uri_match_wildcard; rc = httpd_start(&_redirect_server, &redirect_config); @@ -3424,17 +3457,17 @@ bool WebPanelServer::start() { WEB_PANEL_LOG("redirect server start failed rc=0x%x", static_cast(rc)); } - WEB_PANEL_LOG("server started t=%lu on https://%s/", static_cast(millis()), WiFi.localIP().toString().c_str()); + WEB_PANEL_LOG("server started on https://%s/ (tls certificate: %s)", WiFi.localIP().toString().c_str(), + custom_cert ? "custom" : "built-in"); return true; } void WebPanelServer::stop(bool clear_session) { stopRedirectServer(); if (_server != nullptr) { - httpd_handle_t server = _server; + httpd_ssl_stop(_server); _server = nullptr; WEB_PANEL_LOG("server stopped"); - httpd_ssl_stop(server); } if (clear_session) { _token[0] = 0; @@ -3462,12 +3495,18 @@ void WebPanelServer::noteRestart() { _restart_count++; } +void WebPanelServer::setCustomCert(const char* cert_pem, size_t cert_len, const char* key_pem, size_t key_len) { + _custom_cert = cert_pem; + _custom_cert_len = cert_len; + _custom_key = key_pem; + _custom_key_len = key_len; +} + void WebPanelServer::stopRedirectServer() { if (_redirect_server != nullptr) { - httpd_handle_t redirect_server = _redirect_server; + httpd_stop(_redirect_server); _redirect_server = nullptr; WEB_PANEL_LOG("redirect server stopped"); - httpd_stop(redirect_server); } } @@ -3483,6 +3522,10 @@ void WebPanelServer::lockSession() { _last_activity_ms = 0; } +void WebPanelServer::notifyOtaStarted() { + _ota_started = true; +} + esp_err_t WebPanelServer::handleIndex(httpd_req_t* req) { auto* ctx = static_cast(req->user_ctx); if (ctx == nullptr || ctx->self == nullptr) { @@ -3490,18 +3533,8 @@ esp_err_t WebPanelServer::handleIndex(httpd_req_t* req) { } ctx->self->noteActivity(); httpd_resp_set_type(req, "text/html; charset=utf-8"); - httpd_resp_set_hdr(req, "Cache-Control", "no-store"); markCloseAfterSend(req); - return finishAndClose(req, sendProgmemChunked(req, kWebPanelLoginHtml)); -} - -esp_err_t WebPanelServer::handleFavicon(httpd_req_t* req) { - httpd_resp_set_type(req, "image/png"); - httpd_resp_set_hdr(req, "Cache-Control", "max-age=86400"); - markCloseAfterSend(req); - return finishAndClose(req, httpd_resp_send(req, - reinterpret_cast(eastmesh_web_assets::kFaviconPng), - eastmesh_web_assets::kFaviconPngLen)); + return finishAndClose(req, sendProgmem(req, kWebPanelLoginHtml)); } esp_err_t WebPanelServer::handleHttpRedirect(httpd_req_t* req) { @@ -3527,9 +3560,8 @@ esp_err_t WebPanelServer::handleApp(httpd_req_t* req) { } ctx->self->noteActivity(); httpd_resp_set_type(req, "text/html; charset=utf-8"); - httpd_resp_set_hdr(req, "Cache-Control", "no-store"); markCloseAfterSend(req); - return finishAndClose(req, sendProgmemChunked(req, kWebPanelAppHtml)); + return finishAndClose(req, sendProgmem(req, kWebPanelAppHtml)); } esp_err_t WebPanelServer::handleStatsPage(httpd_req_t* req) { @@ -3539,12 +3571,129 @@ esp_err_t WebPanelServer::handleStatsPage(httpd_req_t* req) { } ctx->self->noteActivity(); httpd_resp_set_type(req, "text/html; charset=utf-8"); + if (ctx->self->_runner != nullptr && !ctx->self->_runner->isWebStatsEnabled()) { + markCloseAfterSend(req); + return finishAndClose(req, sendProgmem(req, kWebPanelStatsDisabledHtml)); + } + markCloseAfterSend(req); + return finishAndClose(req, sendProgmem(req, kWebPanelAppHtml)); +} + +esp_err_t WebPanelServer::handleOtaRedirect(httpd_req_t* req) { + auto* ctx = static_cast(req->user_ctx); + if (ctx == nullptr || ctx->self == nullptr) { + return httpd_resp_send_500(req); + } + if (!ctx->self->_ota_started) { + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "OTA not started"); + return ESP_OK; + } + + char host[64] = {}; + if (httpd_req_get_hdr_value_str(req, "Host", host, sizeof(host)) == ESP_OK && host[0] != '\0') { + char* colon = strchr(host, ':'); + if (colon != nullptr) *colon = '\0'; + } else { + strncpy(host, WiFi.localIP().toString().c_str(), sizeof(host) - 1); + host[sizeof(host) - 1] = '\0'; + } + char location[80]; + snprintf(location, sizeof(location), "http://%s/update", host); + + httpd_resp_set_status(req, "302 Found"); + httpd_resp_set_hdr(req, "Location", location); httpd_resp_set_hdr(req, "Cache-Control", "no-store"); markCloseAfterSend(req); - if (ctx->self->_runner != nullptr && !ctx->self->_runner->isWebStatsEnabled()) { - return finishAndClose(req, sendProgmemChunked(req, kWebPanelStatsDisabledHtml)); + return finishAndClose(req, httpd_resp_send(req, "", 0)); +} + +esp_err_t WebPanelServer::handleOtaUpload(httpd_req_t* req) { + auto* ctx = static_cast(req->user_ctx); + if (ctx == nullptr || ctx->self == nullptr) { + return httpd_resp_send_500(req); } - return finishAndClose(req, sendProgmemChunked(req, kWebPanelAppHtml)); + if (!ctx->self->isAuthorized(req)) { + markCloseAfterSend(req); + return finishAndClose(req, httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized")); + } + + ctx->self->noteActivity(); + + int remaining = req->content_len; + if (remaining <= 0) { + return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "No content"); + } + + constexpr size_t kChunkSize = 4096; + char* buf = allocScratchBuffer(kChunkSize); + if (buf == nullptr) { + return httpd_resp_send_500(req); + } + + if (!Update.begin(static_cast(remaining), U_FLASH)) { + freeScratchBuffer(buf); + return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Update begin failed"); + } + + while (remaining > 0) { + int to_read = (remaining < (int)kChunkSize) ? remaining : (int)kChunkSize; + int n = httpd_req_recv(req, buf, to_read); + if (n <= 0) { + Update.abort(); + freeScratchBuffer(buf); + return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Read error"); + } + if (Update.write(reinterpret_cast(buf), n) != (size_t)n) { + Update.abort(); + freeScratchBuffer(buf); + return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Write error"); + } + remaining -= n; + } + freeScratchBuffer(buf); + + if (!Update.end()) { + return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Update end failed"); + } + + (void)httpd_resp_sendstr(req, "OK"); + vTaskDelay(pdMS_TO_TICKS(1000)); + esp_restart(); +} + +esp_err_t WebPanelServer::handleCertUpload(httpd_req_t* req) { + auto* ctx = static_cast(req->user_ctx); + if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) { + return httpd_resp_send_500(req); + } + if (!ctx->self->isAuthorized(req)) { + markCloseAfterSend(req); + return finishAndClose(req, httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized")); + } + ctx->self->noteActivity(); + if (req->content_len <= 0 || req->content_len > static_cast(kWebCertBundleMax)) { + return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "PEM bundle missing or larger than 12 KB"); + } + char* body = allocScratchBuffer(kWebCertBundleMax + 1); + if (body == nullptr) { + return httpd_resp_send_500(req); + } + if (!ctx->self->readRequestBody(req, body, kWebCertBundleMax + 1)) { + freeScratchBuffer(body); + return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Read error"); + } + char err[96]; + err[0] = 0; + const bool ok = ctx->self->_runner->installWebCertBundle(body, strlen(body), err, sizeof(err)); + freeScratchBuffer(body); + httpd_resp_set_type(req, "text/plain; charset=utf-8"); + httpd_resp_set_hdr(req, "Cache-Control", "no-store"); + markCloseAfterSend(req); + if (!ok) { + httpd_resp_set_status(req, "400 Bad Request"); + return finishAndClose(req, httpd_resp_sendstr(req, err[0] ? err : "Certificate rejected")); + } + return finishAndClose(req, httpd_resp_sendstr(req, "OK - certificate installed, panel restarting")); } esp_err_t WebPanelServer::handleLogin(httpd_req_t* req) { @@ -3580,22 +3729,6 @@ esp_err_t WebPanelServer::handleLogin(httpd_req_t* req) { return finishAndClose(req, httpd_resp_sendstr(req, ctx->self->_token)); } -esp_err_t WebPanelServer::handleSession(httpd_req_t* req) { - auto* ctx = static_cast(req->user_ctx); - if (ctx == nullptr || ctx->self == nullptr) { - return httpd_resp_send_500(req); - } - if (!ctx->self->isAuthorized(req)) { - markCloseAfterSend(req); - return finishAndClose(req, httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized")); - } - - ctx->self->noteActivity(); - httpd_resp_set_type(req, "application/json; charset=utf-8"); - httpd_resp_set_hdr(req, "Cache-Control", "no-store"); - return httpd_resp_sendstr(req, "{\"authenticated\":true}"); -} - esp_err_t WebPanelServer::handleCommand(httpd_req_t* req) { auto* ctx = static_cast(req->user_ctx); if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) { @@ -3631,76 +3764,6 @@ esp_err_t WebPanelServer::handleCommand(httpd_req_t* req) { return rc; } -esp_err_t WebPanelServer::handleFirmwareUpdate(httpd_req_t* req) { - auto* ctx = static_cast(req->user_ctx); - if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) { - return httpd_resp_send_500(req); - } - if (!ctx->self->isAuthorized(req)) { - markCloseAfterSend(req); - return finishAndClose(req, httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized")); - } - if (req->content_len <= 0) { - return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing firmware body"); - } - - char md5[33] = {0}; - if (httpd_req_get_hdr_value_str(req, "X-Firmware-MD5", md5, sizeof(md5)) == ESP_OK && md5[0] != 0) { - if (!Update.setMD5(md5)) { - return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid firmware MD5"); - } - } - - if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) { - Update.printError(Serial); - return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Firmware update could not begin"); - } - - uint8_t* chunk = reinterpret_cast(allocScratchBuffer(kWebFirmwareChunkSize)); - if (chunk == nullptr) { - Update.abort(); - return httpd_resp_send_500(req); - } - - int remaining = req->content_len; - bool ok = true; - while (remaining > 0) { - const int to_read = remaining > static_cast(kWebFirmwareChunkSize) - ? static_cast(kWebFirmwareChunkSize) - : remaining; - const int read = httpd_req_recv(req, reinterpret_cast(chunk), to_read); - if (read <= 0) { - ok = false; - break; - } - if (Update.write(chunk, read) != static_cast(read)) { - Update.printError(Serial); - ok = false; - break; - } - remaining -= read; - } - freeScratchBuffer(chunk); - - if (!ok || remaining != 0) { - Update.abort(); - return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Firmware upload failed"); - } - if (!Update.end(true)) { - Update.printError(Serial); - return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Firmware update failed"); - } - - ctx->self->noteActivity(); - httpd_resp_set_type(req, "text/plain; charset=utf-8"); - httpd_resp_set_hdr(req, "Cache-Control", "no-store"); - esp_err_t rc = httpd_resp_send(req, "OK", HTTPD_RESP_USE_STRLEN); - if (rc == ESP_OK) { - xTaskCreate(rebootAfterFirmwareUpdateTask, "web-fw-reboot", 2048, nullptr, 1, nullptr); - } - return rc; -} - esp_err_t WebPanelServer::handleStats(httpd_req_t* req) { auto* ctx = static_cast(req->user_ctx); if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) { @@ -3827,6 +3890,9 @@ bool WebPanelServer::shouldAutoLock(unsigned long) const { void WebPanelServer::lockSession() { } +void WebPanelServer::notifyOtaStarted() { +} + bool WebPanelServer::isIdle(unsigned long, unsigned long) const { return true; } @@ -3838,4 +3904,7 @@ bool WebPanelServer::isHeapStarved() { void WebPanelServer::noteRestart() { } +void WebPanelServer::setCustomCert(const char*, size_t, const char*, size_t) { +} + #endif diff --git a/src/helpers/web/WebPanelServer.h b/src/helpers/web/WebPanelServer.h index c26c6fc3..e5bf1c02 100644 --- a/src/helpers/web/WebPanelServer.h +++ b/src/helpers/web/WebPanelServer.h @@ -18,6 +18,15 @@ public: virtual ~WebPanelCommandRunner() = default; virtual void runWebCommand(const char* command, char* reply, size_t reply_size) = 0; virtual const char* getWebAdminPassword() const = 0; + // Install a PEM bundle (certificate chain + RSA key) as the panel's TLS certificate. + virtual bool installWebCertBundle(const char* pem, size_t len, char* err, size_t err_size) { + (void)pem; + (void)len; + if (err != nullptr && err_size > 0) { + err[0] = 0; + } + return false; + } virtual bool isWebStatsEnabled() const { return false; } virtual bool formatWebStatsSummaryJson(char* reply, size_t reply_size) { if (reply != nullptr && reply_size > 0) { @@ -46,6 +55,9 @@ public: bool hasSessionToken() const; bool shouldAutoLock(unsigned long now_ms) const; void lockSession(); + void notifyOtaStarted(); + // PEM buffers (lengths include the NUL) used instead of the built-in certificate on the next start(). + void setCustomCert(const char* cert_pem, size_t cert_len, const char* key_pem, size_t key_len); // True when no request has touched the panel for quiet_ms (or ever). bool isIdle(unsigned long now_ms, unsigned long quiet_ms) const; // True when the largest internal heap block can no longer fit a TLS handshake. @@ -70,20 +82,25 @@ private: httpd_handle_t _redirect_server; char _token[33]; unsigned long _last_activity_ms; + RouteContext _route_context; + bool _ota_started; unsigned int _start_deferred_count; unsigned int _restart_count; - RouteContext _route_context; + const char* _custom_cert; + size_t _custom_cert_len; + const char* _custom_key; + size_t _custom_key_len; static esp_err_t handleIndex(httpd_req_t* req); - static esp_err_t handleFavicon(httpd_req_t* req); static esp_err_t handleHttpRedirect(httpd_req_t* req); static esp_err_t handleApp(httpd_req_t* req); static esp_err_t handleStatsPage(httpd_req_t* req); static esp_err_t handleLogin(httpd_req_t* req); - static esp_err_t handleSession(httpd_req_t* req); static esp_err_t handleCommand(httpd_req_t* req); - static esp_err_t handleFirmwareUpdate(httpd_req_t* req); static esp_err_t handleStats(httpd_req_t* req); + static esp_err_t handleOtaRedirect(httpd_req_t* req); + static esp_err_t handleOtaUpload(httpd_req_t* req); + static esp_err_t handleCertUpload(httpd_req_t* req); bool readRequestBody(httpd_req_t* req, char* buffer, size_t buffer_size) const; void refreshToken(); diff --git a/src/helpers/web/WebService.cpp b/src/helpers/web/WebService.cpp index 05dc4176..3dfc3154 100644 --- a/src/helpers/web/WebService.cpp +++ b/src/helpers/web/WebService.cpp @@ -16,13 +16,17 @@ constexpr unsigned long kWebHealCheckMillis = 10000; WebService::WebService() : _fs(nullptr), _prefs{}, _runner(nullptr), _network(nullptr), _suspended_for_ota(false), _last_start_attempt_ms(0), - _last_heal_ms(0), _last_heal_check_ms(0) { + _last_heal_ms(0), _last_heal_check_ms(0), _certs(), _restart_pending(false), _restart_at_ms(0) { WebPrefsStore::setDefaults(_prefs); } void WebService::begin(FILESYSTEM* fs) { _fs = fs; WebPrefsStore::load(_fs, _prefs); + _certs.begin(fs); + if (_certs.hasCustom()) { + _panel.setCustomCert(_certs.certPem(), _certs.certLen(), _certs.keyPem(), _certs.keyLen()); + } } void WebService::end() { @@ -42,6 +46,7 @@ void WebService::prepareForOTAStart() { #if defined(ESP_PLATFORM) && WITH_WEB_PANEL _suspended_for_ota = true; _panel.stopRedirectServer(); + _panel.notifyOtaStarted(); #endif } @@ -53,6 +58,15 @@ void WebService::loop() { _panel.lockSession(); } healIfStarved(now_ms); + if (_restart_pending && static_cast(now_ms - _restart_at_ms) >= 0) { + // Certificate changed: the HTTP response has been flushed by now, restart the listener. + _restart_pending = false; + _panel.stop(false); + _certs.releaseRetired(); + _panel.setCustomCert(_certs.certPem(), _certs.certLen(), _certs.keyPem(), _certs.keyLen()); + _last_start_attempt_ms = 0; + Serial.printf("[WEB] restarting panel with %s TLS certificate\n", _certs.hasCustom() ? "custom" : "built-in"); + } #endif } @@ -70,18 +84,56 @@ bool WebService::setWebEnabled(bool enabled) { if (!enabled) { _suspended_for_ota = false; } - bool ok = savePrefs(); -#if defined(ESP_PLATFORM) && WITH_WEB_PANEL - if (_prefs.web_enabled != 0 && !_suspended_for_ota) { - _last_start_attempt_ms = 0; // manual enable bypasses the retry backoff - ensureWebServer(); - } else { - _panel.stop(); + _last_start_attempt_ms = 0; // manual enable bypasses the retry backoff + return savePrefs(); +} + +void WebService::scheduleRestart() { + _restart_pending = true; + _restart_at_ms = millis() + 800; +} + +bool WebService::installCertBundle(const char* pem, size_t len, char* err, size_t err_size) { + if (!_certs.install(pem, len, err, err_size)) { + return false; } -#endif + scheduleRestart(); + return true; +} + +bool WebService::resetCert() { + const bool ok = _certs.reset(); + scheduleRestart(); return ok; } +void WebService::formatCertStatusReply(char* reply, size_t reply_size) { + const char* source = _certs.hasCustom() ? "custom" : "builtin"; + WebCertStore::Info info; + if (!_certs.describeActive(info)) { + snprintf(reply, reply_size, "> src:%s (certificate unreadable)", source); + return; + } + snprintf(reply, reply_size, "> src:%s cn:%s issuer:%s exp:%s key:%s fp:%s", source, info.subject_cn, + info.issuer_cn, info.valid_to, info.key_type, info.fingerprint); +} + +bool WebService::certInputBegin() { + return _certs.inputBegin(); +} + +bool WebService::certInputAdd(const char* line) { + return _certs.inputAdd(line); +} + +bool WebService::certInputEnd(char* err, size_t err_size) { + if (!_certs.inputEnd(err, err_size)) { + return false; + } + scheduleRestart(); + return true; +} + bool WebService::setWebStatsEnabled(bool enabled) { _prefs.web_stats_enabled = enabled ? 1 : 0; return savePrefs(); @@ -114,20 +166,18 @@ void WebService::formatWebStatusReply(char* reply, size_t reply_size) const { #if defined(ESP_PLATFORM) && WITH_WEB_PANEL void WebService::ensureWebServer() { - if (_suspended_for_ota) { - if (_panel.isRunning()) { - _panel.stopRedirectServer(); - } - return; - } if (_runner == nullptr || _prefs.web_enabled == 0) { _panel.stop(); return; } if (_network == nullptr || !_network->isWifiConnected()) { + // Wi-Fi blip: stop the listener but keep the session so the operator is not logged out. _panel.stop(false); return; } + if (_suspended_for_ota) { + return; + } if (_panel.isRunning()) { return; } diff --git a/src/helpers/web/WebService.h b/src/helpers/web/WebService.h index 3b4b7cd4..52c582e7 100644 --- a/src/helpers/web/WebService.h +++ b/src/helpers/web/WebService.h @@ -4,6 +4,7 @@ #include #include +#include "WebCertStore.h" #include "WebPanelServer.h" #include "WebPrefs.h" @@ -21,7 +22,7 @@ public: void setNetworkStateProvider(NetworkStateProvider* network) { _network = network; } bool setWebEnabled(bool enabled); - bool isWebEnabled() const { return _prefs.web_enabled != 0; } + bool isWebEnabled() const { return _prefs.web_enabled != 0 || _panel.isRunning(); } bool setWebStatsEnabled(bool enabled); bool isWebStatsEnabled() const { return _prefs.web_stats_enabled != 0; } @@ -29,12 +30,21 @@ public: bool isPanelRunning() const { return _panel.isRunning(); } bool isPanelUnlocked() const { return _panel.hasSessionToken(); } + // Custom TLS certificate (see WebCertStore). Changes restart the HTTPS listener shortly after. + bool installCertBundle(const char* pem, size_t len, char* err, size_t err_size); + bool resetCert(); + void formatCertStatusReply(char* reply, size_t reply_size); + bool certInputBegin(); + bool certInputAdd(const char* line); + bool certInputEnd(char* err, size_t err_size); + private: #if defined(ESP_PLATFORM) && WITH_WEB_PANEL void ensureWebServer(); void healIfStarved(unsigned long now_ms); #endif bool savePrefs(); + void scheduleRestart(); FILESYSTEM* _fs; WebPrefs _prefs; @@ -45,4 +55,7 @@ private: unsigned long _last_start_attempt_ms; unsigned long _last_heal_ms; unsigned long _last_heal_check_ms; + WebCertStore _certs; + bool _restart_pending; + unsigned long _restart_at_ms; }; diff --git a/uv.lock b/uv.lock index ebe6f9a4..feaecf55 100644 --- a/uv.lock +++ b/uv.lock @@ -210,7 +210,7 @@ wheels = [ ] [[package]] -name = "meshcore-eastmesh" +name = "meshcore-posadmesh" version = "0.1.0" source = { virtual = "." } dependencies = [ diff --git a/variants/diy_esp32s3_n16r8_e22_back2back/DIYESP32S3N16R8E22Back2backBoard.h b/variants/diy_esp32s3_n16r8_e22_back2back/DIYESP32S3N16R8E22Back2backBoard.h new file mode 100644 index 00000000..5d1749a8 --- /dev/null +++ b/variants/diy_esp32s3_n16r8_e22_back2back/DIYESP32S3N16R8E22Back2backBoard.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include + +class DIYESP32S3N16R8E22Back2backBoard : public ESP32Board { +public: + void begin() { + ESP32Board::begin(); + + esp_reset_reason_t reason = esp_reset_reason(); + if (reason == ESP_RST_DEEPSLEEP) { + long wakeup_source = esp_sleep_get_ext1_wakeup_status(); + if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep) + startup_reason = BD_STARTUP_RX_PACKET; + } + + rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); + rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); + } + } + + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + uint16_t getBattMilliVolts() override { + return 0; + } + + const char* getManufacturerName() const override { + return "DIY ESP32S3 N16R8 E22 Back2back"; + } +}; diff --git a/variants/diy_esp32s3_n16r8_e22_back2back/platformio.ini b/variants/diy_esp32s3_n16r8_e22_back2back/platformio.ini new file mode 100644 index 00000000..99457823 --- /dev/null +++ b/variants/diy_esp32s3_n16r8_e22_back2back/platformio.ini @@ -0,0 +1,234 @@ +[DIY_ESP32S3_N16R8_E22_Back2back] +extends = esp32_base +board = diy_esp32s3_n16r8_e22_back2back +build_flags = + ${esp32_base.build_flags} + ${sensor_base.build_flags} + -I variants/diy_esp32s3_n16r8_e22_back2back + -D DIY_ESP32S3_N16R8_E22_Back2back + -D USE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D P_LORA_DIO_1=14 + -D P_LORA_NSS=46 + -D P_LORA_RESET=12 + -D P_LORA_BUSY=13 + -D P_LORA_SCLK=9 + -D P_LORA_MISO=11 + -D P_LORA_MOSI=10 + -D SX126X_TXEN=47 + -D SX126X_RXEN=48 + -D LORA_TX_POWER=7 ; configured as 7dbm, because the final output will be ~27dbm (~0.5w) if the PA is enabled. + -D MAX_LORA_TX_POWER=19 ; max output without burning out the PA + -D P_LORA_TX_LED=2 ; free pin (led TX) + -D PIN_BOARD_SDA=5 + -D PIN_BOARD_SCL=4 + -D PIN_USER_BTN=1 ; free pin (none Button key) + -D PIN_GPS_RX=3 ; free pin (none GPS) + -D PIN_GPS_TX=8 ; free pin (none GPS) + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/diy_esp32s3_n16r8_e22_back2back> + + +lib_deps = + ${esp32_base.lib_deps} + ${sensor_base.lib_deps} + +[env:DIY_ESP32S3_N16R8_E22_Back2back_repeater] +extends = DIY_ESP32S3_N16R8_E22_Back2back +build_flags = + ${DIY_ESP32S3_N16R8_E22_Back2back.build_flags} + -D ADVERT_NAME='"DIY ESP32S3 N16R8 E22 Back2back Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${DIY_ESP32S3_N16R8_E22_Back2back.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${DIY_ESP32S3_N16R8_E22_Back2back.lib_deps} + ${esp32_ota.lib_deps} + +; [env:DIY_ESP32S3_N16R8_E22_Back2back_repeater_bridge_rs232] +; extends = DIY_ESP32S3_N16R8_E22_Back2back +; build_flags = +; ${DIY_ESP32S3_N16R8_E22_Back2back.build_flags} +; -D ADVERT_NAME='"RS232 Bridge"' +; -D ADVERT_LAT=0.0 +; -D ADVERT_LON=0.0 +; -D ADMIN_PASSWORD='"password"' +; -D MAX_NEIGHBOURS=50 +; -D WITH_RS232_BRIDGE=Serial2 +; -D WITH_RS232_BRIDGE_RX=5 +; -D WITH_RS232_BRIDGE_TX=6 +; -D BRIDGE_DEBUG=1 +; ; -D MESH_PACKET_LOGGING=1 +; ; -D MESH_DEBUG=1 +; build_src_filter = ${DIY_ESP32S3_N16R8_E22_Back2back.build_src_filter} +; + +; +<../examples/simple_repeater> +; lib_deps = +; ${DIY_ESP32S3_N16R8_E22_Back2back.lib_deps} +; ${esp32_ota.lib_deps} + +[env:DIY_ESP32S3_N16R8_E22_Back2back_repeater_bridge_espnow] +extends = DIY_ESP32S3_N16R8_E22_Back2back +build_flags = + ${DIY_ESP32S3_N16R8_E22_Back2back.build_flags} + -D ADVERT_NAME='"ESPNow Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_ESPNOW_BRIDGE=1 +; -D BRIDGE_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${DIY_ESP32S3_N16R8_E22_Back2back.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${DIY_ESP32S3_N16R8_E22_Back2back.lib_deps} + ${esp32_ota.lib_deps} + +[env:DIY_ESP32S3_N16R8_E22_Back2back_logging_repeater] +extends = DIY_ESP32S3_N16R8_E22_Back2back +build_flags = + ${DIY_ESP32S3_N16R8_E22_Back2back.build_flags} + -D ADVERT_NAME='"DIY ESP32S3 N16R8 E22 Back2back Logging Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D MESH_PACKET_LOGGING=1 + -D SX126X_RX_BOOSTED_GAIN=1 +; -D MESH_DEBUG=1 +build_src_filter = ${DIY_ESP32S3_N16R8_E22_Back2back.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${DIY_ESP32S3_N16R8_E22_Back2back.lib_deps} + ${esp32_ota.lib_deps} + +; [env:DIY_ESP32S3_N16R8_E22_Back2back_logging_repeater_bridge_rs232] +; extends = DIY_ESP32S3_N16R8_E22_Back2back +; build_flags = +; ${DIY_ESP32S3_N16R8_E22_Back2back.build_flags} +; -D ADVERT_NAME='"RS232 Bridge"' +; -D ADVERT_LAT=0.0 +; -D ADVERT_LON=0.0 +; -D ADMIN_PASSWORD='"password"' +; -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D SX126X_RX_BOOSTED_GAIN=1 +; -D WITH_RS232_BRIDGE=Serial2 +; -D WITH_RS232_BRIDGE_RX=5 +; -D WITH_RS232_BRIDGE_TX=6 +; -D BRIDGE_DEBUG=1 +; ; -D MESH_DEBUG=1 +; build_src_filter = ${DIY_ESP32S3_N16R8_E22_Back2back.build_src_filter} +; + +; +<../examples/simple_repeater> +; lib_deps = +; ${DIY_ESP32S3_N16R8_E22_Back2back.lib_deps} +; ${esp32_ota.lib_deps} + +[env:DIY_ESP32S3_N16R8_E22_Back2back_logging_repeater_bridge_espnow] +extends = DIY_ESP32S3_N16R8_E22_Back2back +build_flags = + ${DIY_ESP32S3_N16R8_E22_Back2back.build_flags} + -D ADVERT_NAME='"ESPNow Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D MESH_PACKET_LOGGING=1 + -D SX126X_RX_BOOSTED_GAIN=1 + -D WITH_ESPNOW_BRIDGE=1 +; -D BRIDGE_DEBUG=1 +; -D MESH_DEBUG=1 +build_src_filter = ${DIY_ESP32S3_N16R8_E22_Back2back.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${DIY_ESP32S3_N16R8_E22_Back2back.lib_deps} + ${esp32_ota.lib_deps} + +[env:DIY_ESP32S3_N16R8_E22_Back2back_room_server] +extends = DIY_ESP32S3_N16R8_E22_Back2back +build_src_filter = ${DIY_ESP32S3_N16R8_E22_Back2back.build_src_filter} + +<../examples/simple_room_server> +build_flags = + ${DIY_ESP32S3_N16R8_E22_Back2back.build_flags} + -D ADVERT_NAME='"DIY ESP32S3 N16R8 E22 Back2back Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = + ${DIY_ESP32S3_N16R8_E22_Back2back.lib_deps} + ${esp32_ota.lib_deps} + +[env:DIY_ESP32S3_N16R8_E22_Back2back_companion_radio_usb] +extends = DIY_ESP32S3_N16R8_E22_Back2back +build_flags = + ${DIY_ESP32S3_N16R8_E22_Back2back.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 +build_src_filter = ${DIY_ESP32S3_N16R8_E22_Back2back.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${DIY_ESP32S3_N16R8_E22_Back2back.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:DIY_ESP32S3_N16R8_E22_Back2back_companion_radio_ble] +extends = DIY_ESP32S3_N16R8_E22_Back2back +build_flags = + ${DIY_ESP32S3_N16R8_E22_Back2back.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${DIY_ESP32S3_N16R8_E22_Back2back.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${DIY_ESP32S3_N16R8_E22_Back2back.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:DIY_ESP32S3_N16R8_E22_Back2back_companion_radio_wifi] +extends = DIY_ESP32S3_N16R8_E22_Back2back +build_flags = + ${DIY_ESP32S3_N16R8_E22_Back2back.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${DIY_ESP32S3_N16R8_E22_Back2back.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${DIY_ESP32S3_N16R8_E22_Back2back.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/diy_esp32s3_n16r8_e22_back2back/target.cpp b/variants/diy_esp32s3_n16r8_e22_back2back/target.cpp new file mode 100644 index 00000000..121ef941 --- /dev/null +++ b/variants/diy_esp32s3_n16r8_e22_back2back/target.cpp @@ -0,0 +1,57 @@ +#include +#include "target.h" + +DIYESP32S3N16R8E22Back2backBoard board; + +#if defined(P_LORA_SCLK) + static SPIClass spi; + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); + EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else + EnvironmentSensorManager sensors; +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + +#if defined(P_LORA_SCLK) + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); + return radio.std_init(&spi); +#else + return radio.std_init(); +#endif +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(int8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} + diff --git a/variants/diy_esp32s3_n16r8_e22_back2back/target.h b/variants/diy_esp32s3_n16r8_e22_back2back/target.h new file mode 100644 index 00000000..b13e2c24 --- /dev/null +++ b/variants/diy_esp32s3_n16r8_e22_back2back/target.h @@ -0,0 +1,21 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include + +extern DIYESP32S3N16R8E22Back2backBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(int8_t dbm); +mesh::LocalIdentity radio_new_identity(); + diff --git a/variants/meshadventurer/platformio.ini b/variants/meshadventurer/platformio.ini index 4af0f7f7..1ab1e49a 100644 --- a/variants/meshadventurer/platformio.ini +++ b/variants/meshadventurer/platformio.ini @@ -25,6 +25,7 @@ build_flags = -D SX126X_RX_BOOSTED_GAIN=1 -D PIN_GPS_RX=12 -D PIN_GPS_TX=15 + -D OLED_RU=1 -D DISPLAY_CLASS=SSD1306Display build_src_filter = ${esp32_base.build_src_filter} +<../variants/meshadventurer> diff --git a/variants/eastmesh/platformio.ini b/variants/posadmesh_mqtt/platformio.ini similarity index 61% rename from variants/eastmesh/platformio.ini rename to variants/posadmesh_mqtt/platformio.ini index e9ca49d3..c15b6c3d 100644 --- a/variants/eastmesh/platformio.ini +++ b/variants/posadmesh_mqtt/platformio.ini @@ -1,6 +1,6 @@ # ===== Ebyte ===== -[env:Ebyte_EoRa-S3_Repeater_observer] +[env:Ebyte_EoRa-S3_repeater_mqtt] extends = Ebyte_EoRa-S3 board_build.partitions = min_spiffs.csv build_flags = @@ -26,26 +26,26 @@ lib_deps = ${Ebyte_EoRa-S3.lib_deps} ${esp32_ota.lib_deps} -[env:Ebyte_EoRa-S3_Repeater_observer_espnow] -extends = env:Ebyte_EoRa-S3_Repeater_observer +[env:Ebyte_EoRa-S3_repeater_mqtt_espnow] +extends = env:Ebyte_EoRa-S3_repeater_mqtt build_flags = - ${env:Ebyte_EoRa-S3_Repeater_observer.build_flags} + ${env:Ebyte_EoRa-S3_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Ebyte_EoRa-S3_Repeater_observer.build_src_filter} +build_src_filter = ${env:Ebyte_EoRa-S3_repeater_mqtt.build_src_filter} + -[env:Ebyte_EoRa-S3_Repeater_observer_mqtt_bridge] -extends = env:Ebyte_EoRa-S3_Repeater_observer +[env:Ebyte_EoRa-S3_repeater_mqtt_bridge] +extends = env:Ebyte_EoRa-S3_repeater_mqtt build_flags = - ${env:Ebyte_EoRa-S3_Repeater_observer.build_flags} + ${env:Ebyte_EoRa-S3_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Ebyte_EoRa-S3_Repeater_observer.build_src_filter} +build_src_filter = ${env:Ebyte_EoRa-S3_repeater_mqtt.build_src_filter} + # ===== Generic ===== -[env:Generic_E22_sx1262_repeater_observer] +[env:Generic_E22_sx1262_repeater_mqtt] extends = Generic_E22 board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_src_filter = ${Generic_E22.build_src_filter} @@ -72,24 +72,24 @@ lib_deps = ${Generic_E22.lib_deps} ${esp32_ota.lib_deps} -[env:Generic_E22_sx1262_repeater_observer_espnow] -extends = env:Generic_E22_sx1262_repeater_observer +[env:Generic_E22_sx1262_repeater_mqtt_espnow] +extends = env:Generic_E22_sx1262_repeater_mqtt build_flags = - ${env:Generic_E22_sx1262_repeater_observer.build_flags} + ${env:Generic_E22_sx1262_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Generic_E22_sx1262_repeater_observer.build_src_filter} +build_src_filter = ${env:Generic_E22_sx1262_repeater_mqtt.build_src_filter} + -[env:Generic_E22_sx1262_repeater_observer_mqtt_bridge] -extends = env:Generic_E22_sx1262_repeater_observer +[env:Generic_E22_sx1262_repeater_mqtt_bridge] +extends = env:Generic_E22_sx1262_repeater_mqtt build_flags = - ${env:Generic_E22_sx1262_repeater_observer.build_flags} + ${env:Generic_E22_sx1262_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Generic_E22_sx1262_repeater_observer.build_src_filter} +build_src_filter = ${env:Generic_E22_sx1262_repeater_mqtt.build_src_filter} + -[env:Generic_E22_sx1268_repeater_observer] +[env:Generic_E22_sx1268_repeater_mqtt] extends = Generic_E22 board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_src_filter = ${Generic_E22.build_src_filter} @@ -116,26 +116,26 @@ lib_deps = ${Generic_E22.lib_deps} ${esp32_ota.lib_deps} -[env:Generic_E22_sx1268_repeater_observer_espnow] -extends = env:Generic_E22_sx1268_repeater_observer +[env:Generic_E22_sx1268_repeater_mqtt_espnow] +extends = env:Generic_E22_sx1268_repeater_mqtt build_flags = - ${env:Generic_E22_sx1268_repeater_observer.build_flags} + ${env:Generic_E22_sx1268_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Generic_E22_sx1268_repeater_observer.build_src_filter} +build_src_filter = ${env:Generic_E22_sx1268_repeater_mqtt.build_src_filter} + -[env:Generic_E22_sx1268_repeater_observer_mqtt_bridge] -extends = env:Generic_E22_sx1268_repeater_observer +[env:Generic_E22_sx1268_repeater_mqtt_bridge] +extends = env:Generic_E22_sx1268_repeater_mqtt build_flags = - ${env:Generic_E22_sx1268_repeater_observer.build_flags} + ${env:Generic_E22_sx1268_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Generic_E22_sx1268_repeater_observer.build_src_filter} +build_src_filter = ${env:Generic_E22_sx1268_repeater_mqtt.build_src_filter} + # ===== Heltec ===== -[env:Heltec_E213_repeater_observer] +[env:Heltec_E213_repeater_mqtt] extends = Heltec_E213_base build_flags = ${Heltec_E213_base.build_flags} @@ -161,24 +161,24 @@ lib_deps = ${esp32_ota.lib_deps} bakercp/CRC32 @ ^2.0.0 -[env:Heltec_E213_repeater_observer_espnow] -extends = env:Heltec_E213_repeater_observer +[env:Heltec_E213_repeater_mqtt_espnow] +extends = env:Heltec_E213_repeater_mqtt build_flags = - ${env:Heltec_E213_repeater_observer.build_flags} + ${env:Heltec_E213_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Heltec_E213_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_E213_repeater_mqtt.build_src_filter} + -[env:Heltec_E213_repeater_observer_mqtt_bridge] -extends = env:Heltec_E213_repeater_observer +[env:Heltec_E213_repeater_mqtt_bridge] +extends = env:Heltec_E213_repeater_mqtt build_flags = - ${env:Heltec_E213_repeater_observer.build_flags} + ${env:Heltec_E213_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Heltec_E213_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_E213_repeater_mqtt.build_src_filter} + -[env:Heltec_E290_repeater_observer] +[env:Heltec_E290_repeater_mqtt] extends = Heltec_E290_base build_flags = ${Heltec_E290_base.build_flags} @@ -204,24 +204,24 @@ lib_deps = ${esp32_ota.lib_deps} bakercp/CRC32 @ ^2.0.0 -[env:Heltec_E290_repeater_observer_espnow] -extends = env:Heltec_E290_repeater_observer +[env:Heltec_E290_repeater_mqtt_espnow] +extends = env:Heltec_E290_repeater_mqtt build_flags = - ${env:Heltec_E290_repeater_observer.build_flags} + ${env:Heltec_E290_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Heltec_E290_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_E290_repeater_mqtt.build_src_filter} + -[env:Heltec_E290_repeater_observer_mqtt_bridge] -extends = env:Heltec_E290_repeater_observer +[env:Heltec_E290_repeater_mqtt_bridge] +extends = env:Heltec_E290_repeater_mqtt build_flags = - ${env:Heltec_E290_repeater_observer.build_flags} + ${env:Heltec_E290_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Heltec_E290_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_E290_repeater_mqtt.build_src_filter} + -[env:Heltec_T190_repeater_observer] +[env:Heltec_T190_repeater_mqtt] extends = Heltec_T190_base build_flags = ${Heltec_T190_base.build_flags} @@ -244,24 +244,24 @@ lib_deps = ${Heltec_T190_base.lib_deps} ${esp32_ota.lib_deps} -[env:Heltec_T190_repeater_observer_espnow] -extends = env:Heltec_T190_repeater_observer +[env:Heltec_T190_repeater_mqtt_espnow] +extends = env:Heltec_T190_repeater_mqtt build_flags = - ${env:Heltec_T190_repeater_observer.build_flags} + ${env:Heltec_T190_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Heltec_T190_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_T190_repeater_mqtt.build_src_filter} + -[env:Heltec_T190_repeater_observer_mqtt_bridge] -extends = env:Heltec_T190_repeater_observer +[env:Heltec_T190_repeater_mqtt_bridge] +extends = env:Heltec_T190_repeater_mqtt build_flags = - ${env:Heltec_T190_repeater_observer.build_flags} + ${env:Heltec_T190_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Heltec_T190_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_T190_repeater_mqtt.build_src_filter} + -[env:heltec_tracker_v2_repeater_observer] +[env:heltec_tracker_v2_repeater_mqtt] extends = Heltec_tracker_v2 build_flags = ${Heltec_tracker_v2.build_flags} @@ -287,24 +287,24 @@ lib_deps = ${esp32_ota.lib_deps} bakercp/CRC32 @ ^2.0.0 -[env:heltec_tracker_v2_repeater_observer_espnow] -extends = env:heltec_tracker_v2_repeater_observer +[env:heltec_tracker_v2_repeater_mqtt_espnow] +extends = env:heltec_tracker_v2_repeater_mqtt build_flags = - ${env:heltec_tracker_v2_repeater_observer.build_flags} + ${env:heltec_tracker_v2_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:heltec_tracker_v2_repeater_observer.build_src_filter} +build_src_filter = ${env:heltec_tracker_v2_repeater_mqtt.build_src_filter} + -[env:heltec_tracker_v2_repeater_observer_mqtt_bridge] -extends = env:heltec_tracker_v2_repeater_observer +[env:heltec_tracker_v2_repeater_mqtt_bridge] +extends = env:heltec_tracker_v2_repeater_mqtt build_flags = - ${env:heltec_tracker_v2_repeater_observer.build_flags} + ${env:heltec_tracker_v2_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:heltec_tracker_v2_repeater_observer.build_src_filter} +build_src_filter = ${env:heltec_tracker_v2_repeater_mqtt.build_src_filter} + -[env:Heltec_v2_repeater_observer] +[env:Heltec_v2_repeater_mqtt] extends = Heltec_lora32_v2 build_flags = ${Heltec_lora32_v2.build_flags} @@ -330,24 +330,24 @@ lib_deps = ${Heltec_lora32_v2.lib_deps} ${esp32_ota.lib_deps} -[env:Heltec_v2_repeater_observer_espnow] -extends = env:Heltec_v2_repeater_observer +[env:Heltec_v2_repeater_mqtt_espnow] +extends = env:Heltec_v2_repeater_mqtt build_flags = - ${env:Heltec_v2_repeater_observer.build_flags} + ${env:Heltec_v2_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Heltec_v2_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_v2_repeater_mqtt.build_src_filter} + -[env:Heltec_v2_repeater_observer_mqtt_bridge] -extends = env:Heltec_v2_repeater_observer +[env:Heltec_v2_repeater_mqtt_bridge] +extends = env:Heltec_v2_repeater_mqtt build_flags = - ${env:Heltec_v2_repeater_observer.build_flags} + ${env:Heltec_v2_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Heltec_v2_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_v2_repeater_mqtt.build_src_filter} + -[env:Heltec_v3_repeater_observer] +[env:Heltec_v3_repeater_mqtt] extends = Heltec_lora32_v3 build_flags = ${Heltec_lora32_v3.build_flags} @@ -373,24 +373,24 @@ lib_deps = ${esp32_ota.lib_deps} bakercp/CRC32 @ ^2.0.0 -[env:Heltec_v3_repeater_observer_espnow] -extends = env:Heltec_v3_repeater_observer +[env:Heltec_v3_repeater_mqtt_espnow] +extends = env:Heltec_v3_repeater_mqtt build_flags = - ${env:Heltec_v3_repeater_observer.build_flags} + ${env:Heltec_v3_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Heltec_v3_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_v3_repeater_mqtt.build_src_filter} + -[env:Heltec_v3_repeater_observer_mqtt_bridge] -extends = env:Heltec_v3_repeater_observer +[env:Heltec_v3_repeater_mqtt_bridge] +extends = env:Heltec_v3_repeater_mqtt build_flags = - ${env:Heltec_v3_repeater_observer.build_flags} + ${env:Heltec_v3_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Heltec_v3_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_v3_repeater_mqtt.build_src_filter} + -[env:heltec_v4_repeater_observer] +[env:heltec_v4_repeater_mqtt] extends = heltec_v4_oled build_flags = ${heltec_v4_oled.build_flags} @@ -417,24 +417,24 @@ lib_deps = ${esp32_ota.lib_deps} bakercp/CRC32 @ ^2.0.0 -[env:heltec_v4_repeater_observer_espnow] -extends = env:heltec_v4_repeater_observer +[env:heltec_v4_repeater_mqtt_espnow] +extends = env:heltec_v4_repeater_mqtt build_flags = - ${env:heltec_v4_repeater_observer.build_flags} + ${env:heltec_v4_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:heltec_v4_repeater_observer.build_src_filter} +build_src_filter = ${env:heltec_v4_repeater_mqtt.build_src_filter} + -[env:heltec_v4_repeater_observer_mqtt_bridge] -extends = env:heltec_v4_repeater_observer +[env:heltec_v4_repeater_mqtt_bridge] +extends = env:heltec_v4_repeater_mqtt build_flags = - ${env:heltec_v4_repeater_observer.build_flags} + ${env:heltec_v4_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:heltec_v4_repeater_observer.build_src_filter} +build_src_filter = ${env:heltec_v4_repeater_mqtt.build_src_filter} + -[env:heltec_v4_tft_repeater_observer] +[env:heltec_v4_tft_repeater_mqtt] extends = heltec_v4_tft build_flags = ${heltec_v4_tft.build_flags} @@ -460,24 +460,24 @@ lib_deps = ${esp32_ota.lib_deps} bakercp/CRC32 @ ^2.0.0 -[env:heltec_v4_tft_repeater_observer_espnow] -extends = env:heltec_v4_tft_repeater_observer +[env:heltec_v4_tft_repeater_mqtt_espnow] +extends = env:heltec_v4_tft_repeater_mqtt build_flags = - ${env:heltec_v4_tft_repeater_observer.build_flags} + ${env:heltec_v4_tft_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:heltec_v4_tft_repeater_observer.build_src_filter} +build_src_filter = ${env:heltec_v4_tft_repeater_mqtt.build_src_filter} + -[env:heltec_v4_tft_repeater_observer_mqtt_bridge] -extends = env:heltec_v4_tft_repeater_observer +[env:heltec_v4_tft_repeater_mqtt_bridge] +extends = env:heltec_v4_tft_repeater_mqtt build_flags = - ${env:heltec_v4_tft_repeater_observer.build_flags} + ${env:heltec_v4_tft_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:heltec_v4_tft_repeater_observer.build_src_filter} +build_src_filter = ${env:heltec_v4_tft_repeater_mqtt.build_src_filter} + -[env:Heltec_Wireless_Paper_repeater_observer] +[env:Heltec_Wireless_Paper_repeater_mqtt] extends = Heltec_Wireless_Paper_base build_flags = ${Heltec_Wireless_Paper_base.build_flags} @@ -503,24 +503,24 @@ lib_deps = ${esp32_ota.lib_deps} bakercp/CRC32 @ ^2.0.0 -[env:Heltec_Wireless_Paper_repeater_observer_espnow] -extends = env:Heltec_Wireless_Paper_repeater_observer +[env:Heltec_Wireless_Paper_repeater_mqtt_espnow] +extends = env:Heltec_Wireless_Paper_repeater_mqtt build_flags = - ${env:Heltec_Wireless_Paper_repeater_observer.build_flags} + ${env:Heltec_Wireless_Paper_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Heltec_Wireless_Paper_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_Wireless_Paper_repeater_mqtt.build_src_filter} + -[env:Heltec_Wireless_Paper_repeater_observer_mqtt_bridge] -extends = env:Heltec_Wireless_Paper_repeater_observer +[env:Heltec_Wireless_Paper_repeater_mqtt_bridge] +extends = env:Heltec_Wireless_Paper_repeater_mqtt build_flags = - ${env:Heltec_Wireless_Paper_repeater_observer.build_flags} + ${env:Heltec_Wireless_Paper_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Heltec_Wireless_Paper_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_Wireless_Paper_repeater_mqtt.build_src_filter} + -[env:Heltec_Wireless_Tracker_repeater_observer] +[env:Heltec_Wireless_Tracker_repeater_mqtt] extends = Heltec_tracker_base build_flags = ${Heltec_tracker_base.build_flags} @@ -546,24 +546,24 @@ lib_deps = ${Heltec_tracker_base.lib_deps} ${esp32_ota.lib_deps} -[env:Heltec_Wireless_Tracker_repeater_observer_espnow] -extends = env:Heltec_Wireless_Tracker_repeater_observer +[env:Heltec_Wireless_Tracker_repeater_mqtt_espnow] +extends = env:Heltec_Wireless_Tracker_repeater_mqtt build_flags = - ${env:Heltec_Wireless_Tracker_repeater_observer.build_flags} + ${env:Heltec_Wireless_Tracker_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Heltec_Wireless_Tracker_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_Wireless_Tracker_repeater_mqtt.build_src_filter} + -[env:Heltec_Wireless_Tracker_repeater_observer_mqtt_bridge] -extends = env:Heltec_Wireless_Tracker_repeater_observer +[env:Heltec_Wireless_Tracker_repeater_mqtt_bridge] +extends = env:Heltec_Wireless_Tracker_repeater_mqtt build_flags = - ${env:Heltec_Wireless_Tracker_repeater_observer.build_flags} + ${env:Heltec_Wireless_Tracker_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Heltec_Wireless_Tracker_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_Wireless_Tracker_repeater_mqtt.build_src_filter} + -[env:Heltec_WSL3_repeater_observer] +[env:Heltec_WSL3_repeater_mqtt] extends = Heltec_lora32_v3 build_flags = ${Heltec_lora32_v3.build_flags} @@ -587,26 +587,26 @@ lib_deps = ${esp32_ota.lib_deps} bakercp/CRC32 @ ^2.0.0 -[env:Heltec_WSL3_repeater_observer_espnow] -extends = env:Heltec_WSL3_repeater_observer +[env:Heltec_WSL3_repeater_mqtt_espnow] +extends = env:Heltec_WSL3_repeater_mqtt build_flags = - ${env:Heltec_WSL3_repeater_observer.build_flags} + ${env:Heltec_WSL3_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Heltec_WSL3_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_WSL3_repeater_mqtt.build_src_filter} + -[env:Heltec_WSL3_repeater_observer_mqtt_bridge] -extends = env:Heltec_WSL3_repeater_observer +[env:Heltec_WSL3_repeater_mqtt_bridge] +extends = env:Heltec_WSL3_repeater_mqtt build_flags = - ${env:Heltec_WSL3_repeater_observer.build_flags} + ${env:Heltec_WSL3_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Heltec_WSL3_repeater_observer.build_src_filter} +build_src_filter = ${env:Heltec_WSL3_repeater_mqtt.build_src_filter} + # ===== LilyGo ===== -[env:LilyGo_T3S3_sx1262_repeater_observer] +[env:LilyGo_T3S3_sx1262_repeater_mqtt] extends = LilyGo_T3S3_sx1262 board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_flags = @@ -632,24 +632,24 @@ lib_deps = ${LilyGo_T3S3_sx1262.lib_deps} ${esp32_ota.lib_deps} -[env:LilyGo_T3S3_sx1262_repeater_observer_espnow] -extends = env:LilyGo_T3S3_sx1262_repeater_observer +[env:LilyGo_T3S3_sx1262_repeater_mqtt_espnow] +extends = env:LilyGo_T3S3_sx1262_repeater_mqtt build_flags = - ${env:LilyGo_T3S3_sx1262_repeater_observer.build_flags} + ${env:LilyGo_T3S3_sx1262_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:LilyGo_T3S3_sx1262_repeater_observer.build_src_filter} +build_src_filter = ${env:LilyGo_T3S3_sx1262_repeater_mqtt.build_src_filter} + -[env:LilyGo_T3S3_sx1262_repeater_observer_mqtt_bridge] -extends = env:LilyGo_T3S3_sx1262_repeater_observer +[env:LilyGo_T3S3_sx1262_repeater_mqtt_bridge] +extends = env:LilyGo_T3S3_sx1262_repeater_mqtt build_flags = - ${env:LilyGo_T3S3_sx1262_repeater_observer.build_flags} + ${env:LilyGo_T3S3_sx1262_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:LilyGo_T3S3_sx1262_repeater_observer.build_src_filter} +build_src_filter = ${env:LilyGo_T3S3_sx1262_repeater_mqtt.build_src_filter} + -[env:LilyGo_T3S3_sx1276_repeater_observer] +[env:LilyGo_T3S3_sx1276_repeater_mqtt] extends = LilyGo_T3S3_sx1276 board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_flags = @@ -675,24 +675,24 @@ lib_deps = ${LilyGo_T3S3_sx1276.lib_deps} ${esp32_ota.lib_deps} -[env:LilyGo_T3S3_sx1276_repeater_observer_espnow] -extends = env:LilyGo_T3S3_sx1276_repeater_observer +[env:LilyGo_T3S3_sx1276_repeater_mqtt_espnow] +extends = env:LilyGo_T3S3_sx1276_repeater_mqtt build_flags = - ${env:LilyGo_T3S3_sx1276_repeater_observer.build_flags} + ${env:LilyGo_T3S3_sx1276_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:LilyGo_T3S3_sx1276_repeater_observer.build_src_filter} +build_src_filter = ${env:LilyGo_T3S3_sx1276_repeater_mqtt.build_src_filter} + -[env:LilyGo_T3S3_sx1276_repeater_observer_mqtt_bridge] -extends = env:LilyGo_T3S3_sx1276_repeater_observer +[env:LilyGo_T3S3_sx1276_repeater_mqtt_bridge] +extends = env:LilyGo_T3S3_sx1276_repeater_mqtt build_flags = - ${env:LilyGo_T3S3_sx1276_repeater_observer.build_flags} + ${env:LilyGo_T3S3_sx1276_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:LilyGo_T3S3_sx1276_repeater_observer.build_src_filter} +build_src_filter = ${env:LilyGo_T3S3_sx1276_repeater_mqtt.build_src_filter} + -[env:LilyGo_TBeam_1W_repeater_observer] +[env:LilyGo_TBeam_1W_repeater_mqtt] extends = LilyGo_TBeam_1W build_flags = ${LilyGo_TBeam_1W.build_flags} @@ -717,24 +717,24 @@ lib_deps = ${LilyGo_TBeam_1W.lib_deps} ${esp32_ota.lib_deps} -[env:LilyGo_TBeam_1W_repeater_observer_espnow] -extends = env:LilyGo_TBeam_1W_repeater_observer +[env:LilyGo_TBeam_1W_repeater_mqtt_espnow] +extends = env:LilyGo_TBeam_1W_repeater_mqtt build_flags = - ${env:LilyGo_TBeam_1W_repeater_observer.build_flags} + ${env:LilyGo_TBeam_1W_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:LilyGo_TBeam_1W_repeater_observer.build_src_filter} +build_src_filter = ${env:LilyGo_TBeam_1W_repeater_mqtt.build_src_filter} + -[env:LilyGo_TBeam_1W_repeater_observer_mqtt_bridge] -extends = env:LilyGo_TBeam_1W_repeater_observer +[env:LilyGo_TBeam_1W_repeater_mqtt_bridge] +extends = env:LilyGo_TBeam_1W_repeater_mqtt build_flags = - ${env:LilyGo_TBeam_1W_repeater_observer.build_flags} + ${env:LilyGo_TBeam_1W_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:LilyGo_TBeam_1W_repeater_observer.build_src_filter} +build_src_filter = ${env:LilyGo_TBeam_1W_repeater_mqtt.build_src_filter} + -[env:LilyGo_TDeck_repeater_observer] +[env:LilyGo_TDeck_repeater_mqtt] extends = LilyGo_TDeck build_flags = ${LilyGo_TDeck.build_flags} @@ -758,24 +758,24 @@ lib_deps = ${LilyGo_TDeck.lib_deps} ${esp32_ota.lib_deps} -[env:LilyGo_TDeck_repeater_observer_espnow] -extends = env:LilyGo_TDeck_repeater_observer +[env:LilyGo_TDeck_repeater_mqtt_espnow] +extends = env:LilyGo_TDeck_repeater_mqtt build_flags = - ${env:LilyGo_TDeck_repeater_observer.build_flags} + ${env:LilyGo_TDeck_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:LilyGo_TDeck_repeater_observer.build_src_filter} +build_src_filter = ${env:LilyGo_TDeck_repeater_mqtt.build_src_filter} + -[env:LilyGo_TDeck_repeater_observer_mqtt_bridge] -extends = env:LilyGo_TDeck_repeater_observer +[env:LilyGo_TDeck_repeater_mqtt_bridge] +extends = env:LilyGo_TDeck_repeater_mqtt build_flags = - ${env:LilyGo_TDeck_repeater_observer.build_flags} + ${env:LilyGo_TDeck_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:LilyGo_TDeck_repeater_observer.build_src_filter} +build_src_filter = ${env:LilyGo_TDeck_repeater_mqtt.build_src_filter} + -[env:LilyGo_Tlora_C6_repeater_observer] +[env:LilyGo_Tlora_C6_repeater_mqtt] extends = tlora_c6 build_src_filter = ${tlora_c6.build_src_filter} +<../examples/simple_repeater/*.cpp> @@ -797,26 +797,26 @@ build_flags = lib_deps = ${tlora_c6.lib_deps} -[env:LilyGo_Tlora_C6_repeater_observer_espnow] -extends = env:LilyGo_Tlora_C6_repeater_observer +[env:LilyGo_Tlora_C6_repeater_mqtt_espnow] +extends = env:LilyGo_Tlora_C6_repeater_mqtt build_flags = - ${env:LilyGo_Tlora_C6_repeater_observer.build_flags} + ${env:LilyGo_Tlora_C6_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:LilyGo_Tlora_C6_repeater_observer.build_src_filter} +build_src_filter = ${env:LilyGo_Tlora_C6_repeater_mqtt.build_src_filter} + -[env:LilyGo_Tlora_C6_repeater_observer_mqtt_bridge] -extends = env:LilyGo_Tlora_C6_repeater_observer +[env:LilyGo_Tlora_C6_repeater_mqtt_bridge] +extends = env:LilyGo_Tlora_C6_repeater_mqtt build_flags = - ${env:LilyGo_Tlora_C6_repeater_observer.build_flags} + ${env:LilyGo_Tlora_C6_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:LilyGo_Tlora_C6_repeater_observer.build_src_filter} +build_src_filter = ${env:LilyGo_Tlora_C6_repeater_mqtt.build_src_filter} + # ===== M5Stack ===== -[env:M5Stack_Unit_C6L_repeater_observer] +[env:M5Stack_Unit_C6L_repeater_mqtt] extends = M5Stack_Unit_C6L build_src_filter = ${M5Stack_Unit_C6L.build_src_filter} +<../examples/simple_repeater/*.cpp> @@ -838,26 +838,25 @@ build_flags = lib_deps = ${M5Stack_Unit_C6L.lib_deps} -[env:M5Stack_Unit_C6L_repeater_observer_espnow] -extends = env:M5Stack_Unit_C6L_repeater_observer +[env:M5Stack_Unit_C6L_repeater_mqtt_espnow] +extends = env:M5Stack_Unit_C6L_repeater_mqtt build_flags = - ${env:M5Stack_Unit_C6L_repeater_observer.build_flags} + ${env:M5Stack_Unit_C6L_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:M5Stack_Unit_C6L_repeater_observer.build_src_filter} +build_src_filter = ${env:M5Stack_Unit_C6L_repeater_mqtt.build_src_filter} + -[env:M5Stack_Unit_C6L_repeater_observer_mqtt_bridge] -extends = env:M5Stack_Unit_C6L_repeater_observer +[env:M5Stack_Unit_C6L_repeater_mqtt_bridge] +extends = env:M5Stack_Unit_C6L_repeater_mqtt build_flags = - ${env:M5Stack_Unit_C6L_repeater_observer.build_flags} + ${env:M5Stack_Unit_C6L_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:M5Stack_Unit_C6L_repeater_observer.build_src_filter} +build_src_filter = ${env:M5Stack_Unit_C6L_repeater_mqtt.build_src_filter} + # ===== Meshadventurer ===== - -[env:Meshadventurer_sx1262_repeater_observer] +[env:Meshadventurer_sx1262_repeater_mqtt] extends = Meshadventurer build_src_filter = ${Meshadventurer.build_src_filter} +<../examples/simple_repeater> @@ -884,24 +883,100 @@ lib_deps = ${Meshadventurer.lib_deps} ${esp32_ota.lib_deps} -[env:Meshadventurer_sx1262_repeater_observer_espnow] -extends = env:Meshadventurer_sx1262_repeater_observer +[env:Meshadventurer_sx1262_900m33s_repeater_mqtt] +extends = Meshadventurer +build_src_filter = ${Meshadventurer.build_src_filter} + +<../examples/simple_repeater> + + + + + + build_flags = - ${env:Meshadventurer_sx1262_repeater_observer.build_flags} - -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Meshadventurer_sx1262_repeater_observer.build_src_filter} - + + ${Meshadventurer.build_flags} + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=10 + -D ADVERT_NAME='"Meshadventurer Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_UPLINK=1 + -D MQTT_DEBUG=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' + -D MQTT_DEFAULT_IATA='"UNSET"' + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=1 +lib_deps = + ${Meshadventurer.lib_deps} + ${esp32_ota.lib_deps} -[env:Meshadventurer_sx1262_repeater_observer_mqtt_bridge] -extends = env:Meshadventurer_sx1262_repeater_observer +[env:Meshadventurer_sx1262_e22p433_repeater_mqtt] +extends = Meshadventurer +build_src_filter = ${Meshadventurer.build_src_filter} + +<../examples/simple_repeater> + + + + + + build_flags = - ${env:Meshadventurer_sx1262_repeater_observer.build_flags} - -D WITH_MQTT_BRIDGE=1 - -UMQTT_DEBUG -build_src_filter = ${env:Meshadventurer_sx1262_repeater_observer.build_src_filter} - + + ${Meshadventurer.build_flags} + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D SX126X_RXEN=RADIOLIB_NC + -D SX126X_TXEN=RADIOLIB_NC + -D P_LORA_EN=14 + -D LORA_TX_POWER=12 + -D PIN_USER_BTN=39 + -D SX126X_RX_BOOSTED_GAIN=1 + -D ADVERT_NAME='"Meshadventurer Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_UPLINK=1 + -D MQTT_DEBUG=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' + -D MQTT_DEFAULT_IATA='"UNSET"' + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=1 + -D LORA_FREQ=433.0 +lib_deps = + ${Meshadventurer.lib_deps} + ${esp32_ota.lib_deps} -[env:Meshadventurer_sx1268_repeater_observer] +[env:Meshadventurer_sx1262_e22p868_repeater_mqtt] +extends = Meshadventurer +build_src_filter = ${Meshadventurer.build_src_filter} + +<../examples/simple_repeater> + + + + + + +build_flags = + ${Meshadventurer.build_flags} + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D SX126X_RXEN=RADIOLIB_NC + -D SX126X_TXEN=RADIOLIB_NC + -D P_LORA_EN=14 + -D SX126X_DIO2_AS_RF_SWITCH=1 + -D LORA_TX_POWER=12 + -D ADVERT_NAME='"Meshadventurer Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_UPLINK=1 + -D MQTT_DEBUG=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' + -D MQTT_DEFAULT_IATA='"UNSET"' + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = + ${Meshadventurer.lib_deps} + ${esp32_ota.lib_deps} + +[env:Meshadventurer_sx1268_repeater_mqtt] extends = Meshadventurer build_src_filter = ${Meshadventurer.build_src_filter} +<../examples/simple_repeater> @@ -924,30 +999,46 @@ build_flags = -D WIFI_PWD='"mypwd"' -D MQTT_DEFAULT_IATA='"UNSET"' -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=1 + -D EBYTE_E22 + -D MESH_DEBUG=1 + -D E22_DEBUG=1 lib_deps = ${Meshadventurer.lib_deps} ${esp32_ota.lib_deps} -[env:Meshadventurer_sx1268_repeater_observer_espnow] -extends = env:Meshadventurer_sx1268_repeater_observer +[env:Meshadventurer_e22_400m30s_433_repeater_mqtt] +extends = Meshadventurer +build_src_filter = ${Meshadventurer.build_src_filter} + +<../examples/simple_repeater> + + + + + + build_flags = - ${env:Meshadventurer_sx1268_repeater_observer.build_flags} - -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Meshadventurer_sx1268_repeater_observer.build_src_filter} - + - -[env:Meshadventurer_sx1268_repeater_observer_mqtt_bridge] -extends = env:Meshadventurer_sx1268_repeater_observer -build_flags = - ${env:Meshadventurer_sx1268_repeater_observer.build_flags} - -D WITH_MQTT_BRIDGE=1 - -UMQTT_DEBUG -build_src_filter = ${env:Meshadventurer_sx1268_repeater_observer.build_src_filter} - + + ${Meshadventurer.build_flags} + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=12 + -D ADVERT_NAME='"Meshadventurer Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_UPLINK=1 + -D MQTT_DEBUG=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' + -D MQTT_DEFAULT_IATA='"UNSET"' + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=1 + -D EBYTE_E22 + -D LORA_FREQ=433.0 + -D TCXO_OPTIONAL +lib_deps = + ${Meshadventurer.lib_deps} + ${esp32_ota.lib_deps} # ===== Meshimi ===== -[env:Meshimi_repeater_observer] +[env:Meshimi_repeater_mqtt] extends = Meshimi build_src_filter = ${Meshimi.build_src_filter} +<../examples/simple_repeater/*.cpp> @@ -969,18 +1060,18 @@ build_flags = lib_deps = ${Meshimi.lib_deps} -[env:Meshimi_repeater_observer_mqtt_bridge] -extends = env:Meshimi_repeater_observer +[env:Meshimi_repeater_mqtt_bridge] +extends = env:Meshimi_repeater_mqtt build_flags = - ${env:Meshimi_repeater_observer.build_flags} + ${env:Meshimi_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Meshimi_repeater_observer.build_src_filter} +build_src_filter = ${env:Meshimi_repeater_mqtt.build_src_filter} + # ===== Nibble ===== -[env:nibble_screen_connect_repeater_observer] +[env:nibble_screen_connect_repeater_mqtt] extends = nibble_screen_connect_base board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_flags = @@ -1006,26 +1097,26 @@ lib_deps = ${nibble_screen_connect_base.lib_deps} ${esp32_ota.lib_deps} -[env:nibble_screen_connect_repeater_observer_espnow] -extends = env:nibble_screen_connect_repeater_observer +[env:nibble_screen_connect_repeater_mqtt_espnow] +extends = env:nibble_screen_connect_repeater_mqtt build_flags = - ${env:nibble_screen_connect_repeater_observer.build_flags} + ${env:nibble_screen_connect_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:nibble_screen_connect_repeater_observer.build_src_filter} +build_src_filter = ${env:nibble_screen_connect_repeater_mqtt.build_src_filter} + -[env:nibble_screen_connect_repeater_observer_mqtt_bridge] -extends = env:nibble_screen_connect_repeater_observer +[env:nibble_screen_connect_repeater_mqtt_bridge] +extends = env:nibble_screen_connect_repeater_mqtt build_flags = - ${env:nibble_screen_connect_repeater_observer.build_flags} + ${env:nibble_screen_connect_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:nibble_screen_connect_repeater_observer.build_src_filter} +build_src_filter = ${env:nibble_screen_connect_repeater_mqtt.build_src_filter} + # ===== RAK ===== -[env:RAK_3112_repeater_observer] +[env:RAK_3112_repeater_mqtt] extends = rak3112 build_flags = ${rak3112.build_flags} @@ -1049,26 +1140,26 @@ lib_deps = ${esp32_ota.lib_deps} bakercp/CRC32 @ ^2.0.0 -[env:RAK_3112_repeater_observer_espnow] -extends = env:RAK_3112_repeater_observer +[env:RAK_3112_repeater_mqtt_espnow] +extends = env:RAK_3112_repeater_mqtt build_flags = - ${env:RAK_3112_repeater_observer.build_flags} + ${env:RAK_3112_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:RAK_3112_repeater_observer.build_src_filter} +build_src_filter = ${env:RAK_3112_repeater_mqtt.build_src_filter} + -[env:RAK_3112_repeater_observer_mqtt_bridge] -extends = env:RAK_3112_repeater_observer +[env:RAK_3112_repeater_mqtt_bridge] +extends = env:RAK_3112_repeater_mqtt build_flags = - ${env:RAK_3112_repeater_observer.build_flags} + ${env:RAK_3112_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:RAK_3112_repeater_observer.build_src_filter} +build_src_filter = ${env:RAK_3112_repeater_mqtt.build_src_filter} + # ===== Station ===== -[env:Station_G2_repeater_observer] +[env:Station_G2_repeater_mqtt] extends = Station_G2 build_flags = ${Station_G2.build_flags} @@ -1091,24 +1182,24 @@ lib_deps = ${Station_G2.lib_deps} ${esp32_ota.lib_deps} -[env:Station_G2_repeater_observer_espnow] -extends = env:Station_G2_repeater_observer +[env:Station_G2_repeater_mqtt_espnow] +extends = env:Station_G2_repeater_mqtt build_flags = - ${env:Station_G2_repeater_observer.build_flags} + ${env:Station_G2_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Station_G2_repeater_observer.build_src_filter} +build_src_filter = ${env:Station_G2_repeater_mqtt.build_src_filter} + -[env:Station_G2_repeater_observer_mqtt_bridge] -extends = env:Station_G2_repeater_observer +[env:Station_G2_repeater_mqtt_bridge] +extends = env:Station_G2_repeater_mqtt build_flags = - ${env:Station_G2_repeater_observer.build_flags} + ${env:Station_G2_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Station_G2_repeater_observer.build_src_filter} +build_src_filter = ${env:Station_G2_repeater_mqtt.build_src_filter} + -[env:Station_G2_logging_repeater_observer] +[env:Station_G2_logging_repeater_mqtt] extends = Station_G2 build_flags = ${Station_G2.build_flags} @@ -1133,24 +1224,24 @@ lib_deps = ${Station_G2.lib_deps} ${esp32_ota.lib_deps} -[env:Station_G2_logging_repeater_observer_espnow] -extends = env:Station_G2_logging_repeater_observer +[env:Station_G2_logging_repeater_mqtt_espnow] +extends = env:Station_G2_logging_repeater_mqtt build_flags = - ${env:Station_G2_logging_repeater_observer.build_flags} + ${env:Station_G2_logging_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Station_G2_logging_repeater_observer.build_src_filter} +build_src_filter = ${env:Station_G2_logging_repeater_mqtt.build_src_filter} + -[env:Station_G2_logging_repeater_observer_mqtt_bridge] -extends = env:Station_G2_logging_repeater_observer +[env:Station_G2_logging_repeater_mqtt_bridge] +extends = env:Station_G2_logging_repeater_mqtt build_flags = - ${env:Station_G2_logging_repeater_observer.build_flags} + ${env:Station_G2_logging_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Station_G2_logging_repeater_observer.build_src_filter} +build_src_filter = ${env:Station_G2_logging_repeater_mqtt.build_src_filter} + -[env:Station_G3_ESP32_repeater_observer] +[env:Station_G3_ESP32_repeater_mqtt] extends = Station_G3_ESP32 board_build.partitions = min_spiffs.csv ; get around default app partition limit build_flags = @@ -1174,26 +1265,26 @@ lib_deps = ${Station_G3_ESP32.lib_deps} ${esp32_ota.lib_deps} -[env:Station_G3_ESP32_repeater_observer_espnow] -extends = env:Station_G3_ESP32_repeater_observer +[env:Station_G3_ESP32_repeater_mqtt_espnow] +extends = env:Station_G3_ESP32_repeater_mqtt build_flags = - ${env:Station_G3_ESP32_repeater_observer.build_flags} + ${env:Station_G3_ESP32_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Station_G3_ESP32_repeater_observer.build_src_filter} +build_src_filter = ${env:Station_G3_ESP32_repeater_mqtt.build_src_filter} + -[env:Station_G3_ESP32_repeater_observer_mqtt_bridge] -extends = env:Station_G3_ESP32_repeater_observer +[env:Station_G3_ESP32_repeater_mqtt_bridge] +extends = env:Station_G3_ESP32_repeater_mqtt build_flags = - ${env:Station_G3_ESP32_repeater_observer.build_flags} + ${env:Station_G3_ESP32_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Station_G3_ESP32_repeater_observer.build_src_filter} +build_src_filter = ${env:Station_G3_ESP32_repeater_mqtt.build_src_filter} + # ===== T-Beam ===== -[env:T_Beam_S3_Supreme_SX1262_repeater_observer] +[env:T_Beam_S3_Supreme_SX1262_repeater_mqtt] extends = T_Beam_S3_Supreme_SX1262 build_flags = ${T_Beam_S3_Supreme_SX1262.build_flags} @@ -1216,24 +1307,24 @@ lib_deps = ${T_Beam_S3_Supreme_SX1262.lib_deps} ${esp32_ota.lib_deps} -[env:T_Beam_S3_Supreme_SX1262_repeater_observer_espnow] -extends = env:T_Beam_S3_Supreme_SX1262_repeater_observer +[env:T_Beam_S3_Supreme_SX1262_repeater_mqtt_espnow] +extends = env:T_Beam_S3_Supreme_SX1262_repeater_mqtt build_flags = - ${env:T_Beam_S3_Supreme_SX1262_repeater_observer.build_flags} + ${env:T_Beam_S3_Supreme_SX1262_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:T_Beam_S3_Supreme_SX1262_repeater_observer.build_src_filter} +build_src_filter = ${env:T_Beam_S3_Supreme_SX1262_repeater_mqtt.build_src_filter} + -[env:T_Beam_S3_Supreme_SX1262_repeater_observer_mqtt_bridge] -extends = env:T_Beam_S3_Supreme_SX1262_repeater_observer +[env:T_Beam_S3_Supreme_SX1262_repeater_mqtt_bridge] +extends = env:T_Beam_S3_Supreme_SX1262_repeater_mqtt build_flags = - ${env:T_Beam_S3_Supreme_SX1262_repeater_observer.build_flags} + ${env:T_Beam_S3_Supreme_SX1262_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:T_Beam_S3_Supreme_SX1262_repeater_observer.build_src_filter} +build_src_filter = ${env:T_Beam_S3_Supreme_SX1262_repeater_mqtt.build_src_filter} + -[env:Tbeam_SX1262_repeater_observer] +[env:Tbeam_SX1262_repeater_mqtt] extends = LilyGo_TBeam_SX1262 build_flags = ${LilyGo_TBeam_SX1262.build_flags} @@ -1256,24 +1347,24 @@ lib_deps = ${LilyGo_TBeam_SX1262.lib_deps} ${esp32_ota.lib_deps} -[env:Tbeam_SX1262_repeater_observer_espnow] -extends = env:Tbeam_SX1262_repeater_observer +[env:Tbeam_SX1262_repeater_mqtt_espnow] +extends = env:Tbeam_SX1262_repeater_mqtt build_flags = - ${env:Tbeam_SX1262_repeater_observer.build_flags} + ${env:Tbeam_SX1262_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Tbeam_SX1262_repeater_observer.build_src_filter} +build_src_filter = ${env:Tbeam_SX1262_repeater_mqtt.build_src_filter} + -[env:Tbeam_SX1262_repeater_observer_mqtt_bridge] -extends = env:Tbeam_SX1262_repeater_observer +[env:Tbeam_SX1262_repeater_mqtt_bridge] +extends = env:Tbeam_SX1262_repeater_mqtt build_flags = - ${env:Tbeam_SX1262_repeater_observer.build_flags} + ${env:Tbeam_SX1262_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Tbeam_SX1262_repeater_observer.build_src_filter} +build_src_filter = ${env:Tbeam_SX1262_repeater_mqtt.build_src_filter} + -[env:Tbeam_SX1276_repeater_observer] +[env:Tbeam_SX1276_repeater_mqtt] extends = LilyGo_TBeam_SX1276 build_flags = ${LilyGo_TBeam_SX1276.build_flags} @@ -1297,26 +1388,26 @@ lib_deps = ${LilyGo_TBeam_SX1276.lib_deps} ${esp32_ota.lib_deps} -[env:Tbeam_SX1276_repeater_observer_espnow] -extends = env:Tbeam_SX1276_repeater_observer +[env:Tbeam_SX1276_repeater_mqtt_espnow] +extends = env:Tbeam_SX1276_repeater_mqtt build_flags = - ${env:Tbeam_SX1276_repeater_observer.build_flags} + ${env:Tbeam_SX1276_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Tbeam_SX1276_repeater_observer.build_src_filter} +build_src_filter = ${env:Tbeam_SX1276_repeater_mqtt.build_src_filter} + -[env:Tbeam_SX1276_repeater_observer_mqtt_bridge] -extends = env:Tbeam_SX1276_repeater_observer +[env:Tbeam_SX1276_repeater_mqtt_bridge] +extends = env:Tbeam_SX1276_repeater_mqtt build_flags = - ${env:Tbeam_SX1276_repeater_observer.build_flags} + ${env:Tbeam_SX1276_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Tbeam_SX1276_repeater_observer.build_src_filter} +build_src_filter = ${env:Tbeam_SX1276_repeater_mqtt.build_src_filter} + # ===== ThinkNode ===== -[env:ThinkNode_M2_Repeater_observer] +[env:ThinkNode_M2_repeater_mqtt] extends = ThinkNode_M2 board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_src_filter = ${ThinkNode_M2.build_src_filter} @@ -1340,24 +1431,24 @@ lib_deps = ${ThinkNode_M2.lib_deps} ${esp32_ota.lib_deps} -[env:ThinkNode_M2_Repeater_observer_espnow] -extends = env:ThinkNode_M2_Repeater_observer +[env:ThinkNode_M2_repeater_mqtt_espnow] +extends = env:ThinkNode_M2_repeater_mqtt build_flags = - ${env:ThinkNode_M2_Repeater_observer.build_flags} + ${env:ThinkNode_M2_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:ThinkNode_M2_Repeater_observer.build_src_filter} +build_src_filter = ${env:ThinkNode_M2_repeater_mqtt.build_src_filter} + -[env:ThinkNode_M2_Repeater_observer_mqtt_bridge] -extends = env:ThinkNode_M2_Repeater_observer +[env:ThinkNode_M2_repeater_mqtt_bridge] +extends = env:ThinkNode_M2_repeater_mqtt build_flags = - ${env:ThinkNode_M2_Repeater_observer.build_flags} + ${env:ThinkNode_M2_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:ThinkNode_M2_Repeater_observer.build_src_filter} +build_src_filter = ${env:ThinkNode_M2_repeater_mqtt.build_src_filter} + -[env:ThinkNode_M5_Repeater_observer] +[env:ThinkNode_M5_repeater_mqtt] extends = ThinkNode_M5 board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_src_filter = ${ThinkNode_M5.build_src_filter} @@ -1381,26 +1472,26 @@ lib_deps = ${ThinkNode_M5.lib_deps} ${esp32_ota.lib_deps} -[env:ThinkNode_M5_Repeater_observer_espnow] -extends = env:ThinkNode_M5_Repeater_observer +[env:ThinkNode_M5_repeater_mqtt_espnow] +extends = env:ThinkNode_M5_repeater_mqtt build_flags = - ${env:ThinkNode_M5_Repeater_observer.build_flags} + ${env:ThinkNode_M5_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:ThinkNode_M5_Repeater_observer.build_src_filter} +build_src_filter = ${env:ThinkNode_M5_repeater_mqtt.build_src_filter} + -[env:ThinkNode_M5_Repeater_observer_mqtt_bridge] -extends = env:ThinkNode_M5_Repeater_observer +[env:ThinkNode_M5_repeater_mqtt_bridge] +extends = env:ThinkNode_M5_repeater_mqtt build_flags = - ${env:ThinkNode_M5_Repeater_observer.build_flags} + ${env:ThinkNode_M5_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:ThinkNode_M5_Repeater_observer.build_src_filter} +build_src_filter = ${env:ThinkNode_M5_repeater_mqtt.build_src_filter} + # ===== WHY2025 ===== -[env:WHY2025_badge_repeater_observer] +[env:WHY2025_badge_repeater_mqtt] extends = WHY2025_badge build_src_filter = ${WHY2025_badge.build_src_filter} +<../examples/simple_repeater/*.cpp> @@ -1422,26 +1513,26 @@ build_flags = lib_deps = ${WHY2025_badge.lib_deps} -[env:WHY2025_badge_repeater_observer_espnow] -extends = env:WHY2025_badge_repeater_observer +[env:WHY2025_badge_repeater_mqtt_espnow] +extends = env:WHY2025_badge_repeater_mqtt build_flags = - ${env:WHY2025_badge_repeater_observer.build_flags} + ${env:WHY2025_badge_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:WHY2025_badge_repeater_observer.build_src_filter} +build_src_filter = ${env:WHY2025_badge_repeater_mqtt.build_src_filter} + -[env:WHY2025_badge_repeater_observer_mqtt_bridge] -extends = env:WHY2025_badge_repeater_observer +[env:WHY2025_badge_repeater_mqtt_bridge] +extends = env:WHY2025_badge_repeater_mqtt build_flags = - ${env:WHY2025_badge_repeater_observer.build_flags} + ${env:WHY2025_badge_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:WHY2025_badge_repeater_observer.build_src_filter} +build_src_filter = ${env:WHY2025_badge_repeater_mqtt.build_src_filter} + # ===== Xiao ===== -[env:Xiao_C6_repeater_observer] +[env:Xiao_C6_repeater_mqtt] extends = Xiao_C6 build_src_filter = ${Xiao_C6.build_src_filter} +<../examples/simple_repeater/*.cpp> @@ -1463,16 +1554,16 @@ build_flags = lib_deps = ${Xiao_C6.lib_deps} -[env:Xiao_C6_repeater_observer_mqtt_bridge] -extends = env:Xiao_C6_repeater_observer +[env:Xiao_C6_repeater_mqtt_bridge] +extends = env:Xiao_C6_repeater_mqtt build_flags = - ${env:Xiao_C6_repeater_observer.build_flags} + ${env:Xiao_C6_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Xiao_C6_repeater_observer.build_src_filter} +build_src_filter = ${env:Xiao_C6_repeater_mqtt.build_src_filter} + -[env:Xiao_S3_WIO_repeater_observer] +[env:Xiao_S3_WIO_repeater_mqtt] extends = Xiao_S3_WIO build_src_filter = ${Xiao_S3_WIO.build_src_filter} +<../examples/simple_repeater/*.cpp> @@ -1495,24 +1586,24 @@ lib_deps = ${Xiao_S3_WIO.lib_deps} ${esp32_ota.lib_deps} -[env:Xiao_S3_WIO_repeater_observer_espnow] -extends = env:Xiao_S3_WIO_repeater_observer +[env:Xiao_S3_WIO_repeater_mqtt_espnow] +extends = env:Xiao_S3_WIO_repeater_mqtt build_flags = - ${env:Xiao_S3_WIO_repeater_observer.build_flags} + ${env:Xiao_S3_WIO_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Xiao_S3_WIO_repeater_observer.build_src_filter} +build_src_filter = ${env:Xiao_S3_WIO_repeater_mqtt.build_src_filter} + -[env:Xiao_S3_WIO_repeater_observer_mqtt_bridge] -extends = env:Xiao_S3_WIO_repeater_observer +[env:Xiao_S3_WIO_repeater_mqtt_bridge] +extends = env:Xiao_S3_WIO_repeater_mqtt build_flags = - ${env:Xiao_S3_WIO_repeater_observer.build_flags} + ${env:Xiao_S3_WIO_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Xiao_S3_WIO_repeater_observer.build_src_filter} +build_src_filter = ${env:Xiao_S3_WIO_repeater_mqtt.build_src_filter} + -[env:Xiao_S3_LoRa2021F33-2G4_repeater_observer] +[env:Xiao_S3_LoRa2021F33-2G4_repeater_mqtt] extends = Xiao_S3_LoRa2021F33-2G4 build_src_filter = ${Xiao_S3_LoRa2021F33-2G4.build_src_filter} +<../examples/simple_repeater/*.cpp> @@ -1535,19 +1626,19 @@ lib_deps = ${Xiao_S3_LoRa2021F33-2G4.lib_deps} ${esp32_ota.lib_deps} -[env:Xiao_S3_LoRa2021F33-2G4_repeater_observer_espnow] -extends = env:Xiao_S3_LoRa2021F33-2G4_repeater_observer +[env:Xiao_S3_LoRa2021F33-2G4_repeater_mqtt_espnow] +extends = env:Xiao_S3_LoRa2021F33-2G4_repeater_mqtt build_flags = - ${env:Xiao_S3_LoRa2021F33-2G4_repeater_observer.build_flags} + ${env:Xiao_S3_LoRa2021F33-2G4_repeater_mqtt.build_flags} -D WITH_ESPNOW_BRIDGE=1 -build_src_filter = ${env:Xiao_S3_LoRa2021F33-2G4_repeater_observer.build_src_filter} +build_src_filter = ${env:Xiao_S3_LoRa2021F33-2G4_repeater_mqtt.build_src_filter} + -[env:Xiao_S3_LoRa2021F33-2G4_repeater_observer_mqtt_bridge] -extends = env:Xiao_S3_LoRa2021F33-2G4_repeater_observer +[env:Xiao_S3_LoRa2021F33-2G4_repeater_mqtt_bridge] +extends = env:Xiao_S3_LoRa2021F33-2G4_repeater_mqtt build_flags = - ${env:Xiao_S3_LoRa2021F33-2G4_repeater_observer.build_flags} + ${env:Xiao_S3_LoRa2021F33-2G4_repeater_mqtt.build_flags} -D WITH_MQTT_BRIDGE=1 -UMQTT_DEBUG -build_src_filter = ${env:Xiao_S3_LoRa2021F33-2G4_repeater_observer.build_src_filter} +build_src_filter = ${env:Xiao_S3_LoRa2021F33-2G4_repeater_mqtt.build_src_filter} +