Compare commits

..
Author SHA1 Message Date
Ben Meadors 40ef34aa23 Add experimental OLED emulator vuejs component 2026-02-13 20:43:24 -06:00
472 changed files with 9163 additions and 14540 deletions
+1 -1
View File
@@ -100,7 +100,7 @@ runs:
id: version
- name: Store binaries as an artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }}
overwrite: true
+2
View File
@@ -8,6 +8,8 @@ runs:
uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Install dependencies
shell: bash
+3 -3
View File
@@ -4,16 +4,16 @@
- Before starting on some new big chunk of code, it it is optional but highly recommended to open an issue first
to say "Hey, I think this idea X should be implemented and I'm starting work on it. My general plan is Y, any feedback
is appreciated." This will allow other devs to potentially save you time by not accidentally duplicating work etc...
is appreciated." This will allow other devs to potentially save you time by not accidentially duplicating work etc...
- Please do not check in files that don't have real changes
- Please do not reformat lines that you didn't have to change the code on
- We recommend using the [Visual Studio Code](https://platformio.org/install/ide?install=vscode) editor along with the ['Trunk Check' extension](https://marketplace.visualstudio.com/items?itemName=trunk.io) (In beta for windows, WSL2 for the Linux version),
- We recommend using the [Visual Studio Code](https://platformio.org/install/ide?install=vscode) editor along with the ['Trunk Check' extension](https://marketplace.visualstudio.com/items?itemName=trunk.io) (In beta for windows, WSL2 for the linux version),
because it automatically follows our indentation rules and its auto reformatting will not cause spurious changes to lines.
- If your PR fixes a bug, mention "fixes #bugnum" somewhere in your pull request description.
- If your other co-developers have comments on your PR please tweak as needed.
- Please also enable "Allow edits by maintainers".
- Please do not submit untested code.
- If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and community members can help test your changes.
- If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and commnunity members can help test your changes.
- If your PR gets accepted you can request a "Contributor" role in the Meshtastic Discord
## 🤝 Attestations
+7 -5
View File
@@ -16,7 +16,8 @@ on:
type: string
permissions:
contents: read
contents: write
packages: write
jobs:
build-debian-src:
@@ -27,6 +28,8 @@ jobs:
with:
submodules: recursive
path: meshtasticd
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Install deps
shell: bash
@@ -39,8 +42,7 @@ jobs:
sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control
- name: Import GPG key
if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
uses: crazy-max/ghaction-import-gpg@v7
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
id: gpg
@@ -58,11 +60,11 @@ jobs:
run: debian/ci_pack_sdeb.sh
env:
SERIES: ${{ inputs.series }}
GPG_KEY_ID: ${{ steps.gpg.outputs.keyid || '' }}
GPG_KEY_ID: ${{ steps.gpg.outputs.keyid }}
PKG_VERSION: ${{ steps.version.outputs.deb }}
- name: Store binaries as an artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
overwrite: true
+4 -2
View File
@@ -26,6 +26,8 @@ jobs:
- uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Build ${{ inputs.platform }}
id: build
@@ -109,7 +111,7 @@ jobs:
echo "</details>" >> $GITHUB_STEP_SUMMARY
- name: Store binaries as an artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
id: upload-firmware
with:
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}
@@ -125,7 +127,7 @@ jobs:
release/device-*.bat
- name: Store manifests as an artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
id: upload-manifest
with:
name: manifest-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}
+30 -44
View File
@@ -4,14 +4,9 @@ on:
workflow_dispatch:
inputs:
# trunk-ignore(checkov/CKV_GHA_7)
target:
type: string
required: false
description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets.
arch:
type: choice
options:
- all
- esp32
- esp32s3
- esp32c3
@@ -20,18 +15,32 @@ on:
- rp2040
- rp2350
- stm32
description: Choose an arch to limit the search, or 'all' to search all architectures.
default: all
target:
type: string
required: false
description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets.
# find-target:
# type: boolean
# default: true
# description: 'Find the available targets'
permissions: read-all
jobs:
find-targets:
if: ${{ inputs.target == '' }}
strategy:
fail-fast: false
matrix:
arch:
- all
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
@@ -42,37 +51,14 @@ jobs:
- run: pip install -U platformio
- name: Generate matrix
id: jsonStep
env:
BUILDTARGET: ${{ inputs.target }}
MATRIXARCH: ${{ inputs.arch }}
run: |
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level extra)
if [ "$BUILDTARGET" = "" ]; then
echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY
echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY
echo "Arch: $MATRIXARCH" >> $GITHUB_STEP_SUMMARY
echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY
echo "## 🎯 The following target boards are available to build:" >> $GITHUB_STEP_SUMMARY
echo "| Platform | Board |" >> $GITHUB_STEP_SUMMARY
echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY
echo $TARGETS | jq -r 'sort_by(.board) | sort_by(.platform) |.[] | "| " + .platform + " | " + .board + " |" ' >> $GITHUB_STEP_SUMMARY
else
echo "We build this one:" >> $GITHUB_STEP_SUMMARY
ARCH=$(echo "$TARGETS" | jq --arg BUILDTARGET "$BUILDTARGET" -r '.[] | select(.board==$BUILDTARGET) | .platform')
echo "| Platform | Board |" >> $GITHUB_STEP_SUMMARY
echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY
echo "| $ARCH | "$BUILDTARGET" |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ "$ARCH" == "" ]]; then
echo "## ❌ Error: Target "$BUILDTARGET" not found!" >> $GITHUB_STEP_SUMMARY
else
echo "## ✅ Target "$BUILDTARGET" found, proceeding to build." >> $GITHUB_STEP_SUMMARY
fi
echo "You may need to refresh this page to make the built firmware appear below." >> $GITHUB_STEP_SUMMARY
echo "arch=$ARCH" >> $GITHUB_OUTPUT
fi
outputs:
arch: ${{ steps.jsonStep.outputs.arch }}
echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY
echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY
echo "Arch: ${{matrix.arch}}" >> $GITHUB_STEP_SUMMARY
echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY
echo "Targets:" >> $GITHUB_STEP_SUMMARY
echo $TARGETS | jq -r 'sort_by(.board) |.[] | "- " + .board' >> $GITHUB_STEP_SUMMARY
version:
if: ${{ inputs.target != '' }}
@@ -92,16 +78,16 @@ jobs:
build:
if: ${{ inputs.target != '' && inputs.arch != 'native' }}
needs: [version, find-targets]
needs: [version]
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ inputs.target }}
platform: ${{ needs.find-targets.outputs.arch }}
platform: ${{ inputs.arch }}
gather-artifacts:
permissions:
contents: read
contents: write
pull-requests: write
runs-on: ubuntu-latest
needs: [version, build]
@@ -112,7 +98,7 @@ jobs:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
path: ./
pattern: firmware-*-*
@@ -125,7 +111,7 @@ jobs:
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
- name: Repackage in single firmware zip
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }}
overwrite: true
@@ -141,7 +127,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
pattern: firmware-*-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -160,7 +146,7 @@ jobs:
run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip
overwrite: true
+3 -3
View File
@@ -5,7 +5,7 @@ on:
workflow_dispatch:
push:
branches:
- develop # Default branch, same as 'cron' above
- master
paths:
- debian/**
- "*.rpkg"
@@ -16,7 +16,7 @@ on:
- .github/workflows/hook_copr.yml
permissions:
contents: read
contents: write
packages: write
jobs:
@@ -35,8 +35,8 @@ jobs:
series:
- jammy # 22.04 LTS
- noble # 24.04 LTS
- plucky # 25.04
- questing # 25.10
- resolute # 26.04 LTS
uses: ./.github/workflows/package_ppa.yml
with:
ppa_repo: ppa:meshtastic/daily
+8 -6
View File
@@ -37,7 +37,7 @@ on:
value: ${{ jobs.docker-build.outputs.digest }}
permissions:
contents: read
contents: write
packages: write
jobs:
@@ -50,6 +50,8 @@ jobs:
uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Get release version string
run: |
@@ -58,16 +60,16 @@ jobs:
- name: Docker login
if: ${{ inputs.push }}
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
username: meshtastic
password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v3
- name: Docker setup
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Sanitize platform string
id: sanitize_platform
@@ -76,7 +78,7 @@ jobs:
- name: Docker tag
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
images: meshtastic/meshtasticd
tags: |
@@ -84,7 +86,7 @@ jobs:
flavor: latest=false
- name: Docker build and push
uses: docker/build-push-action@v7
uses: docker/build-push-action@v6
id: docker_variant
with:
context: .
+6 -4
View File
@@ -12,7 +12,7 @@ on:
type: string
permissions:
contents: read
contents: write
packages: write
jobs:
@@ -86,6 +86,8 @@ jobs:
uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Get release version string
run: |
@@ -137,14 +139,14 @@ jobs:
id: tags
- name: Docker login
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
username: meshtastic
password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }}
- name: Docker meta (Debian)
id: meta_debian
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
images: meshtastic/meshtasticd
tags: |
@@ -165,7 +167,7 @@ jobs:
- name: Docker meta (Alpine)
id: meta_alpine
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
images: meshtastic/meshtasticd
tags: |
+4 -1
View File
@@ -11,7 +11,8 @@ on:
type: string
permissions:
contents: read
contents: write
packages: write
jobs:
build-copr-hook:
@@ -21,6 +22,8 @@ jobs:
uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{ github.ref }}
repository: ${{ github.repository }}
- name: Trigger COPR build
uses: vidplace7/copr-build@main
+34 -31
View File
@@ -15,7 +15,8 @@ on:
- "**.md"
- version.properties
pull_request:
# Note: This is different from "pull_request". Need to specify ref when doing checkouts.
pull_request_target:
branches:
- master
- develop
@@ -28,8 +29,6 @@ on:
workflow_dispatch:
permissions: read-all
jobs:
setup:
strategy:
@@ -89,6 +88,8 @@ jobs:
- uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Check ${{ matrix.check.board }}
uses: meshtastic/gh-action-firmware@main
with:
@@ -125,16 +126,9 @@ jobs:
test-native:
if: ${{ !contains(github.ref_name, 'event/') && github.repository == 'meshtastic/firmware' }}
permissions: # Needed for dorny/test-reporter.
contents: read
actions: read
checks: write
uses: ./.github/workflows/test_native.yml
docker:
permissions: # Needed for pushing to GHCR.
contents: read
packages: write
strategy:
fail-fast: false
matrix:
@@ -159,6 +153,9 @@ jobs:
gather-artifacts:
# trunk-ignore(checkov/CKV2_GHA_1)
if: github.repository == 'meshtastic/firmware'
permissions:
contents: write
pull-requests: write
strategy:
fail-fast: false
matrix:
@@ -176,8 +173,11 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
path: ./
pattern: firmware-${{matrix.arch}}-*
@@ -187,7 +187,7 @@ jobs:
run: ls -R
- name: Repackage in single firmware zip
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true
@@ -205,7 +205,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -224,13 +224,20 @@ jobs:
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true
path: ./*.elf
retention-days: 30
- uses: scruplelesswizard/comment-artifact@main
if: ${{ github.event_name == 'pull_request' }}
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
github-token: ${{ secrets.GITHUB_TOKEN }}
shame:
if: github.repository == 'meshtastic/firmware'
continue-on-error: true
@@ -238,44 +245,42 @@ jobs:
needs: [build]
steps:
- uses: actions/checkout@v6
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request_target'
with:
filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head)
fetch-depth: 0
- name: Download the current manifests
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
path: ./manifests-new/
pattern: manifest-*
merge-multiple: true
- name: Upload combined manifests for later commit and global stats crunching.
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
id: upload-manifest
with:
name: manifests-${{ github.sha }}
overwrite: true
path: manifests-new/*.mt.json
- name: Find the merge base
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request_target'
run: echo "MERGE_BASE=$(git merge-base "origin/$base" "$head")" >> $GITHUB_ENV
env:
base: ${{ github.base_ref }}
head: ${{ github.sha }}
# Currently broken (for-loop through EVERY artifact -- rate limiting)
# - name: Download the old manifests
# if: github.event_name == 'pull_request'
# if: github.event_name == 'pull_request_target'
# run: gh run download -R "$repo" --name "manifests-$merge_base" --dir manifest-old/
# env:
# GH_TOKEN: ${{ github.token }}
# merge_base: ${{ env.MERGE_BASE }}
# repo: ${{ github.repository }}
# - name: Do scan and post comment
# if: github.event_name == 'pull_request'
# if: github.event_name == 'pull_request_target'
# run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/
release-artifacts:
permissions: # Needed for 'gh release upload'.
contents: write
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
outputs:
@@ -319,14 +324,14 @@ jobs:
body: ${{ steps.release_notes.outputs.notes }}
- name: Download source deb
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true
path: ./output/debian-src
- name: Download `native-tft` pio deps
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -350,7 +355,7 @@ jobs:
}' > firmware-${{ needs.version.outputs.long }}.json
- name: Save Release manifest artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: manifest-${{ needs.version.outputs.long }}
overwrite: true
@@ -367,8 +372,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-firmware:
permissions: # Needed for 'gh release upload'.
contents: write
strategy:
fail-fast: false
matrix:
@@ -393,7 +396,7 @@ jobs:
with:
python-version: 3.x
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -410,7 +413,7 @@ jobs:
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -451,14 +454,14 @@ jobs:
python-version: 3.x
- name: Get firmware artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./publish
- name: Get manifest artifact
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
pattern: manifest-${{ needs.version.outputs.long }}
path: ./publish
+371
View File
@@ -0,0 +1,371 @@
name: Merge Queue
# Not sure how concurrency works in merge_queue, removing for now.
# concurrency:
# group: merge-queue-${{ github.head_ref || github.run_id }}
# cancel-in-progress: true
on:
# Merge group is a special trigger that is used to trigger the workflow when a merge group is created.
merge_group:
jobs:
setup:
strategy:
fail-fast: true
matrix:
arch:
- all
- check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: 3.x
cache: pip
- run: pip install -U platformio
- name: Generate matrix
id: jsonStep
run: |
if [[ "$GITHUB_HEAD_REF" == "" ]]; then
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}})
else
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level pr)
fi
echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF"
echo "${{matrix.arch}}=$TARGETS" >> $GITHUB_OUTPUT
outputs:
all: ${{ steps.jsonStep.outputs.all }}
check: ${{ steps.jsonStep.outputs.check }}
version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Get release version string
run: |
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
id: version
env:
BUILD_LOCATION: local
outputs:
long: ${{ steps.version.outputs.long }}
deb: ${{ steps.version.outputs.deb }}
check:
needs: setup
strategy:
fail-fast: true
matrix:
check: ${{ fromJson(needs.setup.outputs.check) }}
runs-on: ubuntu-latest
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v6
- name: Build base
id: base
uses: ./.github/actions/setup-base
- name: Check ${{ matrix.check.board }}
run: bin/check-all.sh ${{ matrix.check.board }}
build:
needs: [setup, version]
strategy:
matrix:
build: ${{ fromJson(needs.setup.outputs.all) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.build.board }}
platform: ${{ matrix.build.platform }}
build-debian-src:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/build_debian_src.yml
with:
series: UNRELEASED
build_location: local
secrets: inherit
package-pio-deps-native-tft:
if: ${{ github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/package_pio_deps.yml
with:
pio_env: native-tft
secrets: inherit
test-native:
if: ${{ !contains(github.ref_name, 'event/') }}
uses: ./.github/workflows/test_native.yml
docker:
strategy:
fail-fast: false
matrix:
distro: [debian, alpine]
platform: [linux/amd64, linux/arm64, linux/arm/v7]
pio_env: [native, native-tft]
exclude:
- distro: alpine
platform: linux/arm/v7
- pio_env: native-tft
platform: linux/arm64
- pio_env: native-tft
platform: linux/arm/v7
uses: ./.github/workflows/docker_build.yml
with:
distro: ${{ matrix.distro }}
platform: ${{ matrix.platform }}
runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
pio_env: ${{ matrix.pio_env }}
push: false
gather-artifacts:
# trunk-ignore(checkov/CKV2_GHA_1)
permissions:
contents: write
pull-requests: write
strategy:
fail-fast: false
matrix:
arch:
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
runs-on: ubuntu-latest
needs: [version, build]
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v7
with:
path: ./
pattern: firmware-${{matrix.arch}}-*
merge-multiple: true
- name: Display structure of downloaded files
run: ls -R
- name: Move files up
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
- name: Repackage in single firmware zip
uses: actions/upload-artifact@v6
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true
path: |
./firmware-*.bin
./firmware-*.uf2
./firmware-*.hex
./firmware-*.zip
./device-*.sh
./device-*.bat
./littlefs-*.bin
./bleota*bin
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- uses: actions/download-artifact@v7
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./output
# For diagnostics
- name: Show artifacts
run: ls -lR
- name: Device scripts permissions
run: |
chmod +x ./output/device-install.sh || true
chmod +x ./output/device-update.sh || true
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip
uses: actions/upload-artifact@v6
with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true
path: ./*.elf
retention-days: 30
- uses: scruplelesswizard/comment-artifact@main
if: ${{ github.event_name == 'pull_request' }}
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
github-token: ${{ secrets.GITHUB_TOKEN }}
release-artifacts:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
needs:
- version
- gather-artifacts
- build-debian-src
- package-pio-deps-native-tft
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Create release
uses: softprops/action-gh-release@v2
id: create_release
with:
draft: true
prerelease: true
name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha
tag_name: v${{ needs.version.outputs.long }}
body: |
Autogenerated by github action, developer should edit as required before publishing...
- name: Download source deb
uses: actions/download-artifact@v7
with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true
path: ./output/debian-src
- name: Download `native-tft` pio deps
uses: actions/download-artifact@v7
with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./output/pio-deps-native-tft
- name: Zip Linux sources
working-directory: output
run: |
zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src
zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft
# For diagnostics
- name: Display structure of downloaded files
run: ls -lR
- name: Add Linux sources to GtiHub Release
# Only run when targeting master branch with workflow_dispatch
if: ${{ github.ref_name == 'master' }}
run: |
gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip
gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-firmware:
strategy:
fail-fast: false
matrix:
arch:
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
needs: [release-artifacts, version]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- uses: actions/download-artifact@v7
with:
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./output
- name: Display structure of downloaded files
run: ls -lR
- name: Device scripts permissions
run: |
chmod +x ./output/device-install.sh || true
chmod +x ./output/device-update.sh || true
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- uses: actions/download-artifact@v7
with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./elfs
- name: Zip debug elfs
run: zip -j -9 -r ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./elfs
# For diagnostics
- name: Display structure of downloaded files
run: ls -lR
- name: Add bins and debug elfs to GitHub Release
# Only run when targeting master branch with workflow_dispatch
if: ${{ github.ref_name == 'master' }}
run: |
gh release upload v${{ needs.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-firmware:
runs-on: ubuntu-24.04
if: ${{ github.event_name == 'workflow_dispatch' }}
needs: [release-firmware, version]
env:
targets: |-
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- uses: actions/download-artifact@v7
with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./publish
- name: Publish firmware to meshtastic.github.io
uses: peaceiris/actions-gh-pages@v4
env:
# On event/* branches, use the event name as the destination prefix
DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }}
with:
deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }}
external_repository: meshtastic/meshtastic.github.io
publish_branch: master
publish_dir: ./publish
destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }}
keep_files: true
user_name: github-actions[bot]
user_email: github-actions[bot]@users.noreply.github.com
commit_message: ${{ needs.version.outputs.long }}
enable_jekyll: true
+3 -2
View File
@@ -18,7 +18,8 @@ on:
type: string
permissions:
contents: read
contents: write
packages: write
jobs:
build-debian-src:
@@ -57,7 +58,7 @@ jobs:
id: version
- name: Download artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
merge-multiple: true
+5 -12
View File
@@ -16,7 +16,8 @@ on:
type: string
permissions:
contents: read
contents: write
packages: write
jobs:
pkg-pio-libdeps:
@@ -26,6 +27,8 @@ jobs:
uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Setup Python
uses: actions/setup-python@v6
@@ -51,19 +54,9 @@ jobs:
PLATFORMIO_LIBDEPS_DIR: pio/libdeps
PLATFORMIO_PACKAGES_DIR: pio/packages
PLATFORMIO_CORE_DIR: pio/core
PLATFORMIO_SETTING_ENABLE_TELEMETRY: 0
PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL: 3650
PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD: 10240
- name: Mangle platformio cache
# Add "1" to epoch-timestamps of all downloads in the cache.
# This is a hack to prevent internet access at build-time.
run: |
cp pio/core/.cache/downloads/usage.db pio/core/.cache/downloads/usage.db.bak
jq -c 'with_entries(.value |= (. | tostring + "1" | tonumber))' pio/core/.cache/downloads/usage.db.bak > pio/core/.cache/downloads/usage.db
- name: Store binaries as an artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }}
overwrite: true
+6 -4
View File
@@ -16,7 +16,8 @@ on:
type: string
permissions:
contents: read
contents: write
packages: write
jobs:
build-debian-src:
@@ -35,6 +36,8 @@ jobs:
with:
submodules: recursive
path: meshtasticd
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Install deps
shell: bash
@@ -43,7 +46,7 @@ jobs:
sudo apt-get install -y dput
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v7
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
id: gpg
@@ -57,7 +60,7 @@ jobs:
id: version
- name: Download artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
merge-multiple: true
@@ -67,6 +70,5 @@ jobs:
- name: Publish with dput
if: ${{ github.event_name != 'pull_request_target' && github.event_name != 'pull_request' }}
timeout-minutes: 15 # dput is terrible, sometimes runs 'forever'
run: |
dput ${{ inputs.ppa_repo }} meshtasticd_${{ steps.version.outputs.deb }}~${{ inputs.series }}_source.changes
+1 -1
View File
@@ -50,7 +50,7 @@ jobs:
- name: Download test artifacts
if: needs.native-tests.result != 'skipped'
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
name: platformio-test-report-${{ steps.version.outputs.long }}
merge-multiple: true
+1 -1
View File
@@ -23,8 +23,8 @@ jobs:
series:
- jammy # 22.04 LTS
- noble # 24.04 LTS
- plucky # 25.04
- questing # 25.10
- resolute # 26.04 LTS
uses: ./.github/workflows/package_ppa.yml
with:
ppa_repo: |-
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
# step 3
- name: save report as pipeline artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: report.sarif
overwrite: true
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
steps:
- name: Stale PR+Issues
uses: actions/stale@v10.2.0
uses: actions/stale@v10.1.1
with:
days-before-stale: 45
stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days.
+14 -7
View File
@@ -16,6 +16,8 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
submodules: recursive
- name: Setup native build
@@ -57,7 +59,7 @@ jobs:
id: version
- name: Save coverage information
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
if: always() # run this step even if previous step failed
with:
name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }}
@@ -70,6 +72,8 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
submodules: recursive
- name: Setup native build
@@ -90,7 +94,7 @@ jobs:
- name: Save test results
if: always() # run this step even if previous step failed
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: platformio-test-report-${{ steps.version.outputs.long }}
overwrite: true
@@ -104,7 +108,7 @@ jobs:
sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative.
- name: Save coverage information
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
if: always() # run this step even if previous step failed
with:
name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }}
@@ -124,26 +128,29 @@ jobs:
if: always()
steps:
- uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Get release version string
run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
id: version
- name: Download test artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
name: platformio-test-report-${{ steps.version.outputs.long }}
merge-multiple: true
- name: Test Report
uses: dorny/test-reporter@v3.0.0
uses: dorny/test-reporter@v2.5.0
with:
name: PlatformIO Tests
path: testreport.xml
reporter: java-junit
- name: Download coverage artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}
path: code-coverage-report
@@ -156,7 +163,7 @@ jobs:
genhtml --quiet --legend --prefix "${PWD}" code-coverage-report/coverage_src.info --output-directory code-coverage-report
- name: Save Code Coverage Report
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: code-coverage-report-${{ steps.version.outputs.long }}
path: code-coverage-report
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
node-version: 24
- name: Setup pnpm
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@v4
with:
version: latest
+1 -1
View File
@@ -6,7 +6,7 @@ permissions: read-all
jobs:
update-protobufs:
runs-on: ubuntu-latest
permissions: # Needed for peter-evans/create-pull-request.
permissions:
contents: write
pull-requests: write
steps:
-1
View File
@@ -33,7 +33,6 @@ __pycache__
*~
venv/
.venv/
release/
.vscode/extensions.json
/compile_commands.json
+13 -13
View File
@@ -4,31 +4,31 @@ cli:
plugins:
sources:
- id: trunk
ref: v1.7.6
ref: v1.7.4
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- checkov@3.2.513
- renovate@43.100.0
- checkov@3.2.501
- renovate@43.10.3
- prettier@3.8.1
- trufflehog@3.94.1
- trufflehog@3.93.3
- yamllint@1.38.0
- bandit@1.9.4
- trivy@0.69.3
- bandit@1.9.3
- trivy@0.69.1
- taplo@0.10.0
- ruff@0.15.8
- isort@8.0.1
- markdownlint@0.48.0
- ruff@0.15.1
- isort@7.0.0
- markdownlint@0.47.0
- oxipng@10.1.0
- svgo@4.0.1
- actionlint@1.7.11
- svgo@4.0.0
- actionlint@1.7.10
- flake8@7.3.0
- hadolint@2.14.0
- shfmt@3.6.0
- shellcheck@0.11.0
- black@26.3.1
- black@26.1.0
- git-diff-check
- gitleaks@8.30.1
- gitleaks@8.30.0
- clang-format@16.0.3
ignore:
- linters: [ALL]
-26
View File
@@ -1,26 +0,0 @@
# Lightweight container for running native PlatformIO tests on non-Linux hosts
FROM python:3.14-slim-trixie
ENV DEBIAN_FRONTEND=noninteractive
ENV PIP_ROOT_USER_ACTION=ignore
# hadolint ignore=DL3008
RUN apt-get update && apt-get install --no-install-recommends -y \
g++ git ca-certificates pkg-config \
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir platformio==6.1.19 \
&& useradd --create-home --shell /usr/sbin/nologin meshtastic
WORKDIR /firmware
RUN chown -R meshtastic:meshtastic /firmware
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD platformio --version || exit 1
USER meshtastic
# Run tests by default; override with docker run args for specific filters
CMD ["platformio", "test", "-e", "coverage", "-v"]
+1 -1
View File
@@ -23,4 +23,4 @@ for BOARD in $BOARDS; do
CHECK="${CHECK} -e ${BOARD}"
done
pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=low --fail-on-defect=medium --fail-on-defect=high
pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=medium --fail-on-defect=high
-2
View File
@@ -187,7 +187,6 @@ Logging:
LogLevel: info # debug, info, warn, error
# TraceFile: /var/log/meshtasticd.json
# JSONFile: /packets.json # File location for JSON output of decoded packets
# JSONFileRotate: 60 # Rotate JSON file every N minutes, or 0 for no rotation
# JSONFilter: position # filter for packets to save to JSON file
# AsciiLogs: true # default if not specified is !isatty() on stdout
@@ -215,4 +214,3 @@ General:
AvailableDirectory: /etc/meshtasticd/available.d/
# MACAddress: AA:BB:CC:DD:EE:FF
# MACAddressSource: eth0
# APIPort: 4403
@@ -1,9 +1,3 @@
Meta:
name: BananaPi-BPI-R4-sx1262
support: community
compatible:
- bananapi_bpi-r4 # OpenWrt target
Lora:
Module: sx1262 # BananaPi-BPI-R4 SPI via 26p GPIO Header
## CS: 28
@@ -1,10 +1,4 @@
## https://www.mikroe.com/lr-iot-click
Meta:
name: OpenWRT One mikroBUS LR-IOT-CLICK
support: community
compatible:
- openwrt_one # OpenWrt target
Lora:
Module: lr1110 # OpenWRT ONE mikroBUS with LR-IOT-CLICK
# CS: 25
@@ -1,9 +1,3 @@
Meta:
name: OpenWRT One mikroBUS sx1262
support: community
compatible:
- openwrt_one # OpenWrt target
Lora:
Module: sx1262
IRQ: 10
-28
View File
@@ -1,28 +0,0 @@
# meshtasticd configuration files
This directory contains YAML configuration files for meshtasticd. Each file describes a specific hardware configuration, including the LoRa module and pin assignments. These configurations are used by meshtasticd to correctly interface with the hardware.
## Metadata structure
Each configuration file includes a `Meta` section that provides information about the configuration.
This configuration is consumed by configuration-selection tools.
```yaml
Meta:
name: MeshAdv-Pi E22-900M30S # A unique identifier for this configuration.
support: community # community, official, or deprecated; determined by Meshtastic Leads.
compatible: # A list of compatible products or platforms.
- raspberry-pi
```
`name`: A unique identifier for the configuration, typically reflecting the hardware it supports.
`support`: Indicates the level of support for this configuration. It can be one of the following:
- `community`: Supported by the Meshtastic community. Meshtastic Members may not possess, or have not tested this configuration.
- `official`: Fully supported by Meshtastic. Meshtastic Members have tested and verified this configuration.
- `deprecated`: No longer recommended for deployment by Meshtastic.
`compatible`: A list of compatible products or platforms that can use this configuration.
This will vary depending on the intended use case / platform.
Multiple compatible entries can be included. E.g. Armbian `BOARD` value or OpenWrt `TARGET` value.
These tags can be consumed by different configuration-selection tools, filtering based upon their platform/etc.
-6
View File
@@ -1,9 +1,3 @@
Meta:
name: Waveshare 1.44inch LCD HAT
support: community
compatible:
- raspberry-pi
### Waveshare 1.44inch LCD HAT
Display:
Panel: ST7735S
-6
View File
@@ -1,9 +1,3 @@
Meta:
name: Waveshare 2.8inch LCD HAT
support: community
compatible:
- raspberry-pi
Display:
### Waveshare 2.8inch RPi LCD
@@ -1,30 +0,0 @@
---
Lora:
## Ebyte E80-900M22S
## This is a bit experimental
##
##
Module: lr1121
gpiochip: 1 # subtract 32 from the gpio numbers
DIO3_TCXO_VOLTAGE: 1.8
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
rfswitch_table:
pins: [DIO5, DIO6, DIO7]
MODE_STBY: [LOW, LOW, LOW]
MODE_RX: [LOW, HIGH, LOW]
MODE_TX: [HIGH, HIGH, LOW]
MODE_TX_HP: [HIGH, LOW, LOW]
MODE_TX_HF: [LOW, LOW, LOW]
MODE_GNSS: [LOW, LOW, HIGH]
MODE_WIFI: [LOW, LOW, LOW]
General:
MACAddressSource: eth0
@@ -1,46 +0,0 @@
---
Lora:
## Ebyte E80-900M22S
## This is a bit experimental
##
##
Module: lr1121
gpiochip: 1 # subtract 32 from the gpio numbers
DIO3_TCXO_VOLTAGE: 1.8
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
rfswitch_table:
pins:
- DIO5
- DIO6
MODE_STBY:
- LOW
- LOW
MODE_RX:
- HIGH
- LOW
MODE_TX:
- HIGH
- HIGH
MODE_TX_HP:
- LOW
- HIGH
MODE_TX_HF:
- LOW
- LOW
MODE_GNSS:
- LOW
- LOW
MODE_WIFI:
- LOW
- LOW
General:
MACAddressSource: eth0
@@ -1,22 +1,20 @@
---
Meta:
name: Femtofox Ebyte E80-900M22S with TCXO
support: community
compatible:
- luckfox-pico-mini # Armbian
Lora:
## Ebyte E80-900M22S
## This is a bit experimental
##
##
Module: lr1121
gpiochip: 1 # subtract 32 from the gpio numbers
DIO3_TCXO_VOLTAGE: 1.8
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
---
Lora:
## Ebyte E80-900M22S
## This is a bit experimental
##
##
Module: lr1121
gpiochip: 1 # subtract 32 from the gpio numbers
DIO3_TCXO_VOLTAGE: 1.8
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
General:
MACAddressSource: eth0
@@ -1,24 +1,21 @@
---
Meta:
name: Femtofox SX1262 TCXO
support: community
compatible:
- luckfox-pico-mini # Armbian
Lora:
## Ebyte E22-900M30S, E22-900M22S with or without external RF switching setup
## HT-RA62 (Has internal switching, but whatever)
## Seeed WIO SX1262 (already has TXEN-DIO2 link, but needs RXEN)
## Will work with any module with or without RF switching, and with TCXO
Module: sx1262
gpiochip: 1 # subtract 32 from the gpio numbers
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things?
# TXen: bridge to DIO2 on E22 module
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
---
Lora:
## Ebyte E22-900M30S, E22-900M22S with or without external RF switching setup
## HT-RA62 (Has internal switching, but whatever)
## Seeed WIO SX1262 (already has TXEN-DIO2 link, but needs RXEN)
## Will work with any module with or without RF switching, and with TCXO
Module: sx1262
gpiochip: 1 # subtract 32 from the gpio numbers
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things?
# TXen: bridge to DIO2 on E22 module
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
General:
MACAddressSource: eth0
@@ -1,24 +1,21 @@
---
Meta:
name: Femtofox SX1262 XTAL
support: community
compatible:
- luckfox-pico-mini # Armbian
Lora:
## Ebyte E22-900MM22S with no external RF switching setup
## Waveshare SX126X XXXM, AI Thinker RA-01SH
## Will work with any module with or without RF switching and no TCXO
Module: sx1262
gpiochip: 1 # subtract 32 from the gpio numbers
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: false
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things?
# TXen: bridge to DIO2 on E22 module
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
---
Lora:
## Ebyte E22-900MM22S with no external RF switching setup
## Waveshare SX126X XXXM, AI Thinker RA-01SH
## Will work with any module with or without RF switching and no TCXO
Module: sx1262
gpiochip: 1 # subtract 32 from the gpio numbers
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: false
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things?
# TXen: bridge to DIO2 on E22 module
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
General:
MACAddressSource: eth0
@@ -1,30 +0,0 @@
---
Lora:
## Ebyte E80-900M22S
## This is a bit experimental
##
##
Module: lr1121
gpiochip: 1 # subtract 32 from the gpio numbers
DIO3_TCXO_VOLTAGE: 1.8
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
rfswitch_table:
pins: [DIO5, DIO6, DIO7]
MODE_STBY: [LOW, LOW, LOW]
MODE_RX: [LOW, LOW, LOW]
MODE_TX: [LOW, HIGH, LOW]
MODE_TX_HP: [HIGH, LOW, LOW]
# MODE_TX_HF: []
# MODE_GNSS: []
MODE_WIFI: [LOW, LOW, LOW]
General:
MACAddressSource: eth0
-6
View File
@@ -1,9 +1,3 @@
Meta:
name: Adafruit RFM9x
support: deprecated
compatible:
- raspberry-pi
Lora:
Module: RF95 # Adafruit RFM9x
Reset: 25
-6
View File
@@ -1,11 +1,5 @@
# MeshAdv-Pi E22-900M30S
# https://github.com/chrismyers2000/MeshAdv-Pi-Hat
Meta:
name: MeshAdv-Pi E22-900M30S
support: community
compatible:
- raspberry-pi
Lora:
Module: sx1262
CS: 21
@@ -1,11 +1,5 @@
# MeshAdv Mini E22-900M22S
# https://github.com/chrismyers2000/MeshAdv-Mini
Meta:
name: MeshAdv Mini E22-900M22S
support: community
compatible:
- raspberry-pi
Lora:
Module: sx1262 # Ebyte E22-900M22S
CS: 8
+1 -10
View File
@@ -1,20 +1,11 @@
Meta:
name: RAK6421 + RAK13300 Slot 1
support: official
compatible:
- raspberry-pi
Lora:
### RAK13300 in Slot 1
### RAK13300in Slot 1
Module: sx1262
IRQ: 22 #IO6
Reset: 16 # IO4
Busy: 24 # IO5
# Ant_sw: 13 # IO3
Enable_Pins:
- 12
- 13
DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true
spidev: spidev0.0
+1 -10
View File
@@ -1,17 +1,8 @@
Meta:
name: RAK6421 + RAK13300 Slot 2
support: official
compatible:
- raspberry-pi
Lora:
### RAK13300 in Slot 2 pins
### RAK13300in Slot 2 pins
IRQ: 18 #IO6
Reset: 24 # IO4
Busy: 19 # IO5
# Ant_sw: 23 # IO3
Enable_Pins:
- 26
- 23
spidev: spidev0.1
# CS: 7
@@ -1,22 +0,0 @@
Meta:
name: RAK6421 + RAK13302 Slot 1
support: official
compatible:
- raspberry-pi
Lora:
### RAK13302 in Slot 1
Module: sx1262
IRQ: 22 #IO6
Reset: 16 # IO4
Busy: 24 # IO5
# Ant_sw: 13 # IO3
Enable_Pins:
- 12
- 13
DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true
spidev: spidev0.0
# CS: 8
TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8]
@@ -1,18 +0,0 @@
Meta:
name: RAK6421 + RAK13302 Slot 2
support: official
compatible:
- raspberry-pi
Lora:
### RAK13302 in Slot 2 pins
IRQ: 18 #IO6
Reset: 24 # IO4
Busy: 19 # IO5
# Ant_sw: 23 # IO3
Enable_Pins:
- 26
- 23
spidev: spidev0.1
# CS: 7
TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8]
+2 -12
View File
@@ -1,21 +1,11 @@
Meta:
name: RAK6421 + RAK13300 Slot 1 (Autoconf default)
support: official
compatible:
- raspberry-pi
Lora:
### RAK13300 in Slot 1
### RAK13300in Slot 1
Module: sx1262
IRQ: 22 #IO6
Reset: 16 # IO4
Busy: 24 # IO5
Enable_Pins:
- 12
- 13
# Ant_sw: 13 # IO3
DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true
spidev: spidev0.0
# GPIO_DETECT_PA: 13
TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8]
@@ -1,9 +1,3 @@
Meta:
name: Luckfox Lyra PicoCalc Wio LoRa SX1262
support: official
compatible:
- luckfox-lyra-plus # Armbian
Lora:
Module: sx1262
DIO2_AS_RF_SWITCH: true
-23
View File
@@ -1,23 +0,0 @@
# For use with Armbian luckfox-lyra-ultra-w
# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat
# 1 Watt Lyra Ultra hat
Meta:
name: wehooper4 Luckfox Ultra 1W
support: community
compatible:
- luckfox-pico-ultra # Armbian
- luckfox-lyra-ultra # Armbian
Lora:
Module: sx1262
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
CS: 10
IRQ: 5
Busy: 11
Reset: 9
RXen: 14
spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7)
spiSpeed: 2000000
-24
View File
@@ -1,24 +0,0 @@
# For use with Armbian luckfox-lyra-ultra-w
# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat
# 2 Watt Lyra Ultra hat
Meta:
name: wehooper4 Luckfox Ultra 2W
support: community
compatible:
- luckfox-pico-ultra # Armbian
- luckfox-lyra-ultra # Armbian
Lora:
Module: sx1262
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
SX126X_MAX_POWER: 8
CS: 10
IRQ: 5
Busy: 11
Reset: 9
RXen: 14
spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7)
spiSpeed: 2000000
@@ -1,31 +0,0 @@
# For use with Armbian luckfox-lyra // luckfox-lyra-plus
# Enable overlay 'luckfox-lyra-plus-spi0-cs0_rmio13-spidev' with armbian-config
# Waveshare LoRa HAT for Raspberry Pi Pico
# https://www.waveshare.com/wiki/Pico-LoRa-SX1262
Meta:
name: Waveshare LoRa HAT for Raspberry Pi Pico
support: community
compatible:
- luckfox-lyra-plus # Armbian
Lora:
Module: sx1262
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
spidev: spidev0.0
CS: # GPIO0_B5
pin: 13
gpiochip: 0
line: 13
IRQ: # GPIO1_C2
pin: 50
gpiochip: 1
line: 18
Busy: # GPIO0_B4
pin: 12
gpiochip: 0
line: 12
Reset: # GPIO0_A2
pin: 2
gpiochip: 0
line: 2
-6
View File
@@ -1,9 +1,3 @@
Meta:
name: Lora Meshstick SX1262
support: official
compatible:
- usb
Lora:
Module: sx1262
CS: 0
-6
View File
@@ -1,9 +1,3 @@
Meta:
name: Lora Meshstick SX1262
support: community
compatible:
- usb
Lora:
Module: lr1121
CS: 0
+1 -7
View File
@@ -1,11 +1,5 @@
Meta:
name: Pinedio USB SX1262
support: deprecated
compatible:
- usb
Lora:
Module: sx1262
CS: 0
IRQ: 10
spidev: ch341
spidev: ch341
@@ -1,9 +1,3 @@
Meta:
name: raxda-rock2f-starter-edition-hat
support: community
compatible:
- rock-2f # Armbian
Lora:
### Raxda Rock 2F running Armbian Linux 6.1.99-vendor-rk35xx
@@ -1,11 +1,5 @@
# https://www.waveshare.com/core1262-868m.htm
# https://github.com/markbirss/lora-starter-edition-sx1262-i2c
Meta:
name: lora-starter-edition-sx1262-i2c
support: community
compatible:
- raspberry-pi
Lora:
Module: sx1262 # Starter Edition SX1262 I2C Raspberry Pi HAT
DIO2_AS_RF_SWITCH: true
@@ -1,9 +1,3 @@
Meta:
name: meshstick-1262
support: official
compatible:
- usb
Lora:
Module: sx1262
CS: 0
-6
View File
@@ -1,9 +1,3 @@
Meta:
name: meshtoad-e22
support: official
compatible:
- usb
Lora:
Module: sx1262
CS: 0
+2 -7
View File
@@ -1,9 +1,3 @@
Meta:
name: umesh-1262-30dbm
support: community
compatible:
- clockwork-uconsole
Lora:
Module: sx1262
CS: 0
@@ -19,7 +13,8 @@ Lora:
# USB_Serialnum: 12345678
SX126X_MAX_POWER: 22
# Reduce output power to improve EMI
TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7]
NUM_PA_POINTS: 22
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
# Note: This module integrates an additional PA to achieve higher output power.
# The 'power' parameter here does not represent the actual RF output.
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (122 dBm).
+2 -7
View File
@@ -1,9 +1,3 @@
Meta:
name: umesh-1268-30dbm
support: community
compatible:
- clockwork-uconsole
Lora:
Module: sx1268
CS: 0
@@ -19,7 +13,8 @@ Lora:
# USB_Serialnum: 12345678
SX126X_MAX_POWER: 22
# Reduce output power to improve EMI
TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7]
NUM_PA_POINTS: 22
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
# Note: This module integrates an additional PA to achieve higher output power.
# The 'power' parameter here does not represent the actual RF output.
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (122 dBm).
-6
View File
@@ -1,9 +1,3 @@
Meta:
name: Waveshare SX1262
support: deprecated
compatible:
- raspberry-pi
Lora:
Module: sx1262 # Waveshare SX126X XXXM
DIO2_AS_RF_SWITCH: true
@@ -1,12 +1,5 @@
# https://www.waveshare.com/pico-lora-sx1262-868m.htm
# https://github.com/markbirss/lora-ws-raspberry-pi-pico-to-rpi-adapter
Meta:
name: ws-raspberry-pico-to-rpi-adapter
support: community
compatible:
- raspberry-pi
Lora:
Module: sx1262 # Waveshare Raspberry Pi Pico to Raspberry Pi HAT Adapter
DIO2_AS_RF_SWITCH: true
@@ -15,12 +15,6 @@
# 5 CS 24
# 26 DIO1/IRQ 26
Meta:
name: ws-raspberry-pico-to-orangepi-03
support: community
compatible:
- orange-pi-zero-3 # Armbian
Lora:
Module: sx1262 # Waveshare Raspberry Pico Lora module
DIO2_AS_RF_SWITCH: true
+1 -1
View File
@@ -43,7 +43,7 @@ for pio_env in pio_envs:
env = {
"ci": {"board": pio_env, "platform": env_platform},
"board_level": cfg.get(f"env:{pio_env}", "board_level", default=None),
"board_check": cfg.get(f"env:{pio_env}", "board_check", default="false").strip().lower() == "true",
"board_check": bool(cfg.get(f"env:{pio_env}", "board_check", default=False)),
}
all_envs.append(env)
@@ -87,9 +87,6 @@
</screenshots>
<releases>
<release version="2.7.21" date="2026-03-11">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.21</url>
</release>
<release version="2.7.20" date="2026-02-11">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.20</url>
</release>
-44
View File
@@ -1,44 +0,0 @@
#!/usr/bin/env bash
# Run native PlatformIO tests inside Docker (for macOS / non-Linux hosts).
#
# Usage:
# ./bin/test-native-docker.sh # run all native tests
# ./bin/test-native-docker.sh -f test_transmit_history # run specific test filter
# ./bin/test-native-docker.sh --rebuild # force rebuild the image
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
IMAGE_NAME="meshtastic-native-test"
REBUILD=false
EXTRA_ARGS=()
for arg in "$@"; do
if [[ "$arg" == "--rebuild" ]]; then
REBUILD=true
else
EXTRA_ARGS+=("$arg")
fi
done
if $REBUILD || ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then
echo "Building test image (first run may take a few minutes)..."
docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/Dockerfile.test" "$ROOT_DIR"
fi
# Disable BUILD_EPOCH to avoid full rebuilds between test runs (matches CI)
sed_cmd='s/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/'
# Default: run all tests. Pass extra args (e.g. -f test_transmit_history) through.
if [[ ${#EXTRA_ARGS[@]} -eq 0 ]]; then
CMD=("platformio" "test" "-e" "coverage" "-v")
else
CMD=("platformio" "test" "-e" "coverage" "-v" "${EXTRA_ARGS[@]}")
fi
exec docker run --rm \
-v "$ROOT_DIR:/src:ro" \
"$IMAGE_NAME" \
bash -c "rm -rf /tmp/fw-test && cp -a /src /tmp/fw-test && cd /tmp/fw-test && sed -i '${sed_cmd}' platformio.ini && ${CMD[*]}"
-54
View File
@@ -1,54 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x4405"],
["0x239A", "0x0029"],
["0x239A", "0x002A"],
["0x2886", "0x1667"]
],
"usb_product": "HT-n5262G",
"mcu": "nrf52840",
"variant": "heltec_mesh_node_t096",
"variants_dir": "variants",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"onboard_tools": ["jlink"],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "Heltec nrf (Adafruit BSP)",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://heltec.org/",
"vendor": "Heltec"
}
-40
View File
@@ -1,40 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default.csv"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_ESP32S3_DEV",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"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": "LilyGo Mini-Epaper-S3 (4 MB Flash, 2MB PSRAM)",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 4194304,
"require_upload_port": true,
"speed": 460800
},
"url": "https://www.lilygo.cc",
"vendor": "LilyGo"
}
+1 -1
View File
@@ -8,7 +8,7 @@
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=0"
],
-38
View File
@@ -1,38 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi",
"partitions": "default_16MB.csv"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=0",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "LilyGo T5-ePaper-S3",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 921600
},
"url": "https://lilygo.cc/products/t5-e-paper-s3-pro",
"vendor": "LILYGO"
}
-29
View File
@@ -1,29 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "esp32_out.ld"
},
"core": "esp32",
"extra_flags": "-DARDUINO_T_Beam",
"f_cpu": "240000000L",
"f_flash": "40000000L",
"flash_mode": "dio",
"mcu": "esp32",
"variant": "tbeam"
},
"connectivity": ["wifi", "bluetooth", "can", "ethernet"],
"debug": {
"openocd_board": "esp-wroom-32.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "TTGO T-Beam (PSRAM Disabled)",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 1310720,
"maximum_size": 4194304,
"require_upload_port": true,
"speed": 460800
},
"url": "https://github.com/LilyGO/TTGO-T-Beam",
"vendor": "TTGO"
}
+1 -1
View File
@@ -12,6 +12,6 @@ Ex:
- `logo_320x480.png`
- `logo_320x240.png`
This file is copied to `data/boot/logo.png` before filesystem image compilation.
This file is copied to `data/boot/logo.png` before filesytem image compilation.
For additional examples see the [`event/defcon33` branch](https://github.com/meshtastic/firmware/tree/event/defcon33).
-6
View File
@@ -1,9 +1,3 @@
meshtasticd (2.7.21.0) unstable; urgency=medium
* Version 2.7.21
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Wed, 11 Mar 2026 11:45:36 +0000
meshtasticd (2.7.20.0) unstable; urgency=medium
* Version 2.7.20
+2 -14
View File
@@ -3,17 +3,10 @@ export DEBEMAIL="jbennett@incomsystems.biz"
export PLATFORMIO_LIBDEPS_DIR=pio/libdeps
export PLATFORMIO_PACKAGES_DIR=pio/packages
export PLATFORMIO_CORE_DIR=pio/core
export PLATFORMIO_SETTING_ENABLE_TELEMETRY=0
export PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL=3650
export PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD=10240
# Download libraries to `pio`
platformio pkg install -e native-tft
platformio pkg install -e native-tft -t platformio/tool-scons@4.40502.0
# Mangle PlatformIO cache to prevent internet access at build-time
# Simply adds 1 to all expiry (epoch) timestamps, adding ~500 years to expiry date
cp pio/core/.cache/downloads/usage.db pio/core/.cache/downloads/usage.db.bak
jq -c 'with_entries(.value |= (. | tostring + "1" | tonumber))' pio/core/.cache/downloads/usage.db.bak >pio/core/.cache/downloads/usage.db
# Compress `pio` directory to prevent dh_clean from sanitizing it
tar -cf pio.tar pio/
rm -rf pio
@@ -27,10 +20,5 @@ rm -rf debian/changelog
dch --create --distribution "$SERIES" --package "$package" --newversion "$PKG_VERSION~$SERIES" \
"GitHub Actions Automatic packaging for $PKG_VERSION~$SERIES"
if [[ -n $GPG_KEY_ID ]]; then
# Build and sign the source deb
debuild -S -nc -k"$GPG_KEY_ID"
else
# Build the source deb without signing (forks)
debuild -S -nc
fi
# Build the source deb
debuild -S -nc -k"$GPG_KEY_ID"
+1 -4
View File
@@ -9,10 +9,7 @@
PIO_ENV:=\
PLATFORMIO_CORE_DIR=pio/core \
PLATFORMIO_LIBDEPS_DIR=pio/libdeps \
PLATFORMIO_PACKAGES_DIR=pio/packages \
PLATFORMIO_SETTING_ENABLE_TELEMETRY=0 \
PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL=3650 \
PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD=10240
PLATFORMIO_PACKAGES_DIR=pio/packages
# Raspbian armhf builds should be compatible with armv6-hardfloat
# https://www.valvers.com/open-software/raspberry-pi/bare-metal-programming-in-c-part-1/#rpi1-compiler-flags
+30
View File
@@ -0,0 +1,30 @@
# Dependencies
node_modules/
# Build output
dist/
# Vite cache
.vite/
# TypeScript cache
*.tsbuildinfo
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
# Local env files
.env
.env.local
.env.*.local
+149
View File
@@ -0,0 +1,149 @@
# Meshtastic OLED Emulator
A Vue.js component and TypeScript library for emulating SSD1306/SH1106 OLED displays used in Meshtastic devices. This allows firmware UI development and testing directly in the browser.
## Features
- **Same API as firmware**: Uses the same `OLEDDisplay` API as esp8266-oled-ssd1306 library
- **Multiple display sizes**: 128x64, 128x32, 64x48, 128x128
- **Realistic rendering**: Pixel glow, OLED color tints (blue, white, yellow/blue dual)
- **XBM image support**: Render firmware icons and images
- **Font rendering**: Bitmap font support with text alignment
- **Vue 3 component**: Easy integration with Vue applications
## Installation
```bash
npm install @meshtastic/oled-emulator
```
## Quick Start
```vue
<script setup lang="ts">
import { ref, onMounted } from "vue";
import {
OLEDDisplay,
OLEDEmulator,
ArialMT_Plain_10,
} from "@meshtastic/oled-emulator";
// Create display matching firmware
const display = ref(new OLEDDisplay("128x64"));
onMounted(() => {
display.value.setFont(ArialMT_Plain_10);
display.value.clear();
display.value.drawString(10, 10, "Hello Mesh!");
display.value.drawRect(5, 5, 118, 54);
display.value.fillCircle(64, 32, 15);
});
</script>
<template>
<OLEDEmulator
:display="display"
:pixel-scale="4"
preset="ssd1306-blue"
:enable-glow="true"
/>
</template>
```
## OLEDDisplay API
The `OLEDDisplay` class provides the same methods as the firmware's display library:
```typescript
const display = new OLEDDisplay("128x64");
// Basic drawing
display.clear();
display.setPixel(x, y);
display.drawLine(x0, y0, x1, y1);
display.drawRect(x, y, width, height);
display.fillRect(x, y, width, height);
display.drawCircle(x, y, radius);
display.fillCircle(x, y, radius);
// Text
display.setFont(ArialMT_Plain_10);
display.setTextAlignment("TEXT_ALIGN_CENTER");
display.drawString(x, y, "Hello");
display.drawStringMaxWidth(x, y, maxWidth, "Long text...");
const width = display.getStringWidth("text");
// Images (XBM format)
display.drawXbm(x, y, width, height, imageData);
// Colors
display.setColor("WHITE"); // or 'BLACK', 'INVERSE'
// Buffer access (for custom rendering)
const buffer = display.getBuffer(); // Uint8Array
```
## Display Presets
The emulator includes several realistic display presets:
| Preset | Description |
| --------------------- | ------------------------------- |
| `ssd1306-blue` | Classic blue OLED (default) |
| `ssd1306-white` | White OLED |
| `ssd1306-yellow-blue` | Dual-color (top 16 rows yellow) |
| `sh1106` | SH1106 style |
| `sh1107` | SH1107 128x128 |
| `ssd1306-64x48` | Small 64x48 display |
| `ssd1306-128x32` | Wide 128x32 display |
## Screen Renderers
Pre-built screen renderers matching firmware UI:
```typescript
import { ScreenRenderers } from "@meshtastic/oled-emulator";
// Boot screen
ScreenRenderers.drawBootScreen(display, "2.5.0");
// Node info screen
ScreenRenderers.drawNodeInfoScreen(display, {
shortName: "MESH",
longName: "My Node",
nodeId: "!abcd1234",
lastHeard: "2m ago",
snr: 12.5,
});
// Message screen
ScreenRenderers.drawMessageScreen(display, {
from: "Alice",
text: "Hello from the mesh!",
time: "14:32",
channel: "LongFast",
});
// And more: drawGPSScreen, drawNodeListScreen, drawSystemScreen, drawCompassScreen
```
## Development
```bash
# Install dependencies
npm install
# Run development server
npm run dev
# Build library
npm run build
```
## Future: Protocol Bridge
A future enhancement will allow connecting this emulator to a real Meshtastic firmware instance (running on native/portduino), enabling live framebuffer streaming over WebSocket for real-time display mirroring.
## License
GPL-3.0 - Same as Meshtastic firmware
+22
View File
@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Meshtastic OLED Emulator</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #121212;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+2211
View File
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
{
"name": "@meshtastic/oled-emulator",
"version": "0.1.0",
"description": "Vue.js OLED display emulator for Meshtastic firmware UI development",
"keywords": [
"meshtastic",
"oled",
"ssd1306",
"sh1106",
"display",
"emulator",
"vue",
"vue3"
],
"author": "Meshtastic",
"license": "GPL-3.0",
"repository": {
"type": "git",
"url": "https://github.com/meshtastic/firmware.git",
"directory": "oled-emulator"
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./style.css": "./dist/style.css"
},
"files": [
"dist",
"src"
],
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"type-check": "vue-tsc --noEmit"
},
"peerDependencies": {
"vue": "^3.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"typescript": "^5.3.0",
"vite": "^5.0.0",
"vite-plugin-dts": "^3.7.0",
"vue": "^3.4.0",
"vue-tsc": "^2.0.0"
}
}
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
/**
* DisplayPresets.ts - OLED display presets for emulator
*/
import type { OLEDDISPLAY_GEOMETRY } from "./OLEDDisplay";
// Display type presets with realistic OLED colors
export interface DisplayPreset {
name: string;
geometry: OLEDDISPLAY_GEOMETRY;
pixelOn: { r: number; g: number; b: number };
pixelOff: { r: number; g: number; b: number };
glow: number;
contrast: number;
}
export const DISPLAY_PRESETS: Record<string, DisplayPreset> = {
// Classic blue OLED (SSD1306)
"ssd1306-blue": {
name: "SSD1306 Blue",
geometry: "128x64",
pixelOn: { r: 100, g: 180, b: 255 },
pixelOff: { r: 2, g: 3, b: 8 },
glow: 0.4,
contrast: 1.0,
},
// White OLED (SSD1306)
"ssd1306-white": {
name: "SSD1306 White",
geometry: "128x64",
pixelOn: { r: 255, g: 255, b: 245 },
pixelOff: { r: 5, g: 5, b: 5 },
glow: 0.3,
contrast: 1.0,
},
// Yellow-blue dual color OLED
"ssd1306-yellow-blue": {
name: "SSD1306 Yellow/Blue",
geometry: "128x64",
// Yellow section is top 16 rows, blue is rest
pixelOn: { r: 255, g: 220, b: 50 }, // Will be overridden per-pixel
pixelOff: { r: 3, g: 3, b: 5 },
glow: 0.35,
contrast: 1.0,
},
// SH1106 (slightly different characteristics)
sh1106: {
name: "SH1106",
geometry: "128x64",
pixelOn: { r: 120, g: 200, b: 255 },
pixelOff: { r: 2, g: 2, b: 6 },
glow: 0.45,
contrast: 0.95,
},
// SH1107 128x128
sh1107: {
name: "SH1107 128x128",
geometry: "128x128",
pixelOn: { r: 255, g: 255, b: 240 },
pixelOff: { r: 4, g: 4, b: 6 },
glow: 0.3,
contrast: 1.0,
},
// Small 64x48 OLED
"ssd1306-64x48": {
name: "SSD1306 64x48",
geometry: "64x48",
pixelOn: { r: 100, g: 180, b: 255 },
pixelOff: { r: 2, g: 3, b: 8 },
glow: 0.4,
contrast: 1.0,
},
// Mini 128x32
"ssd1306-128x32": {
name: "SSD1306 128x32",
geometry: "128x32",
pixelOn: { r: 100, g: 180, b: 255 },
pixelOff: { r: 2, g: 3, b: 8 },
glow: 0.4,
contrast: 1.0,
},
};
+557
View File
@@ -0,0 +1,557 @@
/**
* OLEDDisplay.ts - TypeScript port of esp8266-oled-ssd1306 OLEDDisplay API
*
* This provides the same drawing API as the firmware uses, allowing
* Meshtastic screen rendering code to be ported to the web.
*
* Buffer format: Vertical byte packing (8 vertical pixels per byte)
* Same as SSD1306/SH1106 native format.
*/
export type OLEDDISPLAY_GEOMETRY =
| "128x64"
| "128x32"
| "64x48"
| "64x32"
| "128x128"
| "96x16";
export type OLEDDISPLAY_COLOR = "WHITE" | "BLACK" | "INVERSE";
export type OLEDDISPLAY_TEXT_ALIGNMENT =
| "TEXT_ALIGN_LEFT"
| "TEXT_ALIGN_CENTER"
| "TEXT_ALIGN_RIGHT";
export interface OLEDFont {
height: number;
firstChar: number;
charCount: number;
widths: number[];
data: Uint8Array;
}
// Resolution info based on geometry
const GEOMETRY_SIZES: Record<
OLEDDISPLAY_GEOMETRY,
{ width: number; height: number }
> = {
"128x64": { width: 128, height: 64 },
"128x32": { width: 128, height: 32 },
"64x48": { width: 64, height: 48 },
"64x32": { width: 64, height: 32 },
"128x128": { width: 128, height: 128 },
"96x16": { width: 96, height: 16 },
};
export class OLEDDisplay {
private buffer: Uint8Array;
private _width: number;
private _height: number;
private color: OLEDDISPLAY_COLOR = "WHITE";
private textAlignment: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_LEFT";
private font: OLEDFont | null = null;
constructor(geometry: OLEDDISPLAY_GEOMETRY = "128x64") {
const size = GEOMETRY_SIZES[geometry];
this._width = size.width;
this._height = size.height;
// Buffer size: width * (height / 8) - vertical byte packing
this.buffer = new Uint8Array((this._width * this._height) / 8);
}
get width(): number {
return this._width;
}
get height(): number {
return this._height;
}
getWidth(): number {
return this._width;
}
getHeight(): number {
return this._height;
}
/**
* Get the raw framebuffer (vertical byte packing format)
*/
getBuffer(): Uint8Array {
return this.buffer;
}
getBufferSize(): number {
return this.buffer.length;
}
/**
* Clear the display buffer
*/
clear(): void {
this.buffer.fill(0);
}
/**
* Set the drawing color
*/
setColor(color: OLEDDISPLAY_COLOR): void {
this.color = color;
}
/**
* Set text alignment for drawString
*/
setTextAlignment(align: OLEDDISPLAY_TEXT_ALIGNMENT): void {
this.textAlignment = align;
}
/**
* Set the current font
*/
setFont(font: OLEDFont): void {
this.font = font;
}
/**
* Set a single pixel
*/
setPixel(x: number, y: number): void {
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return;
// Vertical byte packing: each byte represents 8 vertical pixels
const byteIndex = x + Math.floor(y / 8) * this._width;
const bitMask = 1 << (y & 7);
switch (this.color) {
case "WHITE":
this.buffer[byteIndex] |= bitMask;
break;
case "BLACK":
this.buffer[byteIndex] &= ~bitMask;
break;
case "INVERSE":
this.buffer[byteIndex] ^= bitMask;
break;
}
}
/**
* Clear a single pixel (set to black)
*/
clearPixel(x: number, y: number): void {
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return;
const byteIndex = x + Math.floor(y / 8) * this._width;
const bitMask = 1 << (y & 7);
this.buffer[byteIndex] &= ~bitMask;
}
/**
* Get a pixel value (true = on, false = off)
*/
getPixel(x: number, y: number): boolean {
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return false;
const byteIndex = x + Math.floor(y / 8) * this._width;
const bitMask = 1 << (y & 7);
return (this.buffer[byteIndex] & bitMask) !== 0;
}
/**
* Draw a line using Bresenham's algorithm
*/
drawLine(x0: number, y0: number, x1: number, y1: number): void {
const dx = Math.abs(x1 - x0);
const dy = Math.abs(y1 - y0);
const sx = x0 < x1 ? 1 : -1;
const sy = y0 < y1 ? 1 : -1;
let err = dx - dy;
while (true) {
this.setPixel(x0, y0);
if (x0 === x1 && y0 === y1) break;
const e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
/**
* Draw horizontal line (optimized)
*/
drawHorizontalLine(x: number, y: number, length: number): void {
if (y < 0 || y >= this._height) return;
for (let i = 0; i < length; i++) {
this.setPixel(x + i, y);
}
}
/**
* Draw vertical line (optimized)
*/
drawVerticalLine(x: number, y: number, length: number): void {
if (x < 0 || x >= this._width) return;
for (let i = 0; i < length; i++) {
this.setPixel(x, y + i);
}
}
/**
* Draw a rectangle outline
*/
drawRect(x: number, y: number, width: number, height: number): void {
this.drawHorizontalLine(x, y, width);
this.drawHorizontalLine(x, y + height - 1, width);
this.drawVerticalLine(x, y, height);
this.drawVerticalLine(x + width - 1, y, height);
}
/**
* Draw a filled rectangle
*/
fillRect(x: number, y: number, width: number, height: number): void {
for (let i = 0; i < width; i++) {
for (let j = 0; j < height; j++) {
this.setPixel(x + i, y + j);
}
}
}
/**
* Draw a circle outline using midpoint algorithm
*/
drawCircle(x0: number, y0: number, radius: number): void {
let x = radius;
let y = 0;
let err = 0;
while (x >= y) {
this.setPixel(x0 + x, y0 + y);
this.setPixel(x0 + y, y0 + x);
this.setPixel(x0 - y, y0 + x);
this.setPixel(x0 - x, y0 + y);
this.setPixel(x0 - x, y0 - y);
this.setPixel(x0 - y, y0 - x);
this.setPixel(x0 + y, y0 - x);
this.setPixel(x0 + x, y0 - y);
y++;
if (err <= 0) {
err += 2 * y + 1;
}
if (err > 0) {
x--;
err -= 2 * x + 1;
}
}
}
/**
* Draw a filled circle
*/
fillCircle(x0: number, y0: number, radius: number): void {
let x = radius;
let y = 0;
let err = 0;
while (x >= y) {
this.drawHorizontalLine(x0 - x, y0 + y, 2 * x + 1);
this.drawHorizontalLine(x0 - y, y0 + x, 2 * y + 1);
this.drawHorizontalLine(x0 - x, y0 - y, 2 * x + 1);
this.drawHorizontalLine(x0 - y, y0 - x, 2 * y + 1);
y++;
if (err <= 0) {
err += 2 * y + 1;
}
if (err > 0) {
x--;
err -= 2 * x + 1;
}
}
}
/**
* Draw a quarter circle (quadrant 0-3)
*/
drawCircleQuads(x0: number, y0: number, radius: number, quads: number): void {
let x = radius;
let y = 0;
let err = 0;
while (x >= y) {
if (quads & 0x1) {
this.setPixel(x0 + x, y0 - y);
this.setPixel(x0 + y, y0 - x);
}
if (quads & 0x2) {
this.setPixel(x0 - y, y0 - x);
this.setPixel(x0 - x, y0 - y);
}
if (quads & 0x4) {
this.setPixel(x0 - x, y0 + y);
this.setPixel(x0 - y, y0 + x);
}
if (quads & 0x8) {
this.setPixel(x0 + y, y0 + x);
this.setPixel(x0 + x, y0 + y);
}
y++;
if (err <= 0) {
err += 2 * y + 1;
}
if (err > 0) {
x--;
err -= 2 * x + 1;
}
}
}
/**
* Draw an XBM image (like firmware's drawXbm)
* XBM format: LSB first, horizontal
*/
drawXbm(
x: number,
y: number,
width: number,
height: number,
xbm: Uint8Array | number[],
): void {
const widthBytes = Math.ceil(width / 8);
for (let j = 0; j < height; j++) {
for (let i = 0; i < width; i++) {
const byteIndex = j * widthBytes + Math.floor(i / 8);
const bitIndex = i % 8;
if (xbm[byteIndex] & (1 << bitIndex)) {
this.setPixel(x + i, y + j);
}
}
}
}
/**
* Draw a PROGMEM-style image (vertical byte format, like some firmware icons)
*/
drawFastImage(
x: number,
y: number,
width: number,
height: number,
image: Uint8Array | number[],
): void {
const heightInBytes = Math.ceil(height / 8);
for (let i = 0; i < width; i++) {
for (let j = 0; j < heightInBytes; j++) {
const imageByte = image[i + j * width];
for (let bit = 0; bit < 8; bit++) {
if (imageByte & (1 << bit)) {
this.setPixel(x + i, y + j * 8 + bit);
}
}
}
}
}
/**
* Get the width of a string with the current font
*/
getStringWidth(text: string): number {
if (!this.font) return 0;
let width = 0;
for (const char of text) {
const charCode = char.charCodeAt(0);
if (
charCode >= this.font.firstChar &&
charCode < this.font.firstChar + this.font.charCount
) {
width += this.font.widths[charCode - this.font.firstChar];
} else {
// Unknown char - use space width or default
width += this.font.widths[0] || 4;
}
}
return width;
}
/**
* Draw a string at the given position
*/
drawString(x: number, y: number, text: string): void {
if (!this.font) return;
// Adjust x based on alignment
let startX = x;
if (this.textAlignment === "TEXT_ALIGN_CENTER") {
startX = x - Math.floor(this.getStringWidth(text) / 2);
} else if (this.textAlignment === "TEXT_ALIGN_RIGHT") {
startX = x - this.getStringWidth(text);
}
let cursorX = startX;
for (const char of text) {
const charCode = char.charCodeAt(0);
if (
charCode >= this.font.firstChar &&
charCode < this.font.firstChar + this.font.charCount
) {
const charIndex = charCode - this.font.firstChar;
const charWidth = this.font.widths[charIndex];
// Find the offset into the font data
let offset = 0;
const bytesPerColumn = Math.ceil(this.font.height / 8);
for (let i = 0; i < charIndex; i++) {
offset += this.font.widths[i] * bytesPerColumn;
}
// Draw the character
this.drawFontChar(
cursorX,
y,
charWidth,
this.font.height,
this.font.data,
offset,
);
cursorX += charWidth;
} else {
// Unknown character - advance by space
cursorX += this.font.widths[0] || 4;
}
}
}
/**
* Draw a single font character
*/
private drawFontChar(
x: number,
y: number,
width: number,
height: number,
data: Uint8Array,
offset: number,
): void {
const bytesPerColumn = Math.ceil(height / 8);
for (let col = 0; col < width; col++) {
for (let byteRow = 0; byteRow < bytesPerColumn; byteRow++) {
const dataByte = data[offset + col * bytesPerColumn + byteRow];
for (let bit = 0; bit < 8; bit++) {
const pixelY = byteRow * 8 + bit;
if (pixelY < height && dataByte & (1 << bit)) {
this.setPixel(x + col, y + pixelY);
}
}
}
}
}
/**
* Draw a string that wraps within maxWidth
*/
drawStringMaxWidth(
x: number,
y: number,
maxWidth: number,
text: string,
): void {
if (!this.font) return;
const words = text.split(" ");
let line = "";
let lineY = y;
for (const word of words) {
const testLine = line + (line ? " " : "") + word;
const testWidth = this.getStringWidth(testLine);
if (testWidth > maxWidth && line) {
this.drawString(x, lineY, line);
line = word;
lineY += this.font.height;
} else {
line = testLine;
}
}
if (line) {
this.drawString(x, lineY, line);
}
}
/**
* Draw a progress bar
*/
drawProgressBar(
x: number,
y: number,
width: number,
height: number,
progress: number,
): void {
// Clamp progress 0-100
progress = Math.max(0, Math.min(100, progress));
// Draw outline
this.drawRect(x, y, width, height);
// Draw fill
const fillWidth = Math.floor(((width - 4) * progress) / 100);
this.fillRect(x + 2, y + 2, fillWidth, height - 4);
}
/**
* Display - no-op for emulator (would send buffer to hardware)
*/
display(): void {
// In real hardware this sends the buffer to the display
// For emulator, this is a no-op - we render from getBuffer()
}
/**
* Flip the display (useful for some mounting orientations)
*/
flipScreenVertically(): void {
// Rotate buffer 180 degrees
const newBuffer = new Uint8Array(this.buffer.length);
const heightBytes = this._height / 8;
for (let x = 0; x < this._width; x++) {
for (let yByte = 0; yByte < heightBytes; yByte++) {
let srcByte = this.buffer[x + yByte * this._width];
// Reverse the bits
let reversed = 0;
for (let i = 0; i < 8; i++) {
if (srcByte & (1 << i)) {
reversed |= 1 << (7 - i);
}
}
// Place in mirrored position
const newX = this._width - 1 - x;
const newYByte = heightBytes - 1 - yByte;
newBuffer[newX + newYByte * this._width] = reversed;
}
}
this.buffer = newBuffer;
}
}
// Color constants for compatibility with firmware code style
export const WHITE: OLEDDISPLAY_COLOR = "WHITE";
export const BLACK: OLEDDISPLAY_COLOR = "BLACK";
export const INVERSE: OLEDDISPLAY_COLOR = "INVERSE";
export const TEXT_ALIGN_LEFT: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_LEFT";
export const TEXT_ALIGN_CENTER: OLEDDISPLAY_TEXT_ALIGNMENT =
"TEXT_ALIGN_CENTER";
export const TEXT_ALIGN_RIGHT: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_RIGHT";
+327
View File
@@ -0,0 +1,327 @@
<script setup lang="ts">
/**
* OLEDEmulator.vue - Vue 3 component for OLED display emulation
*
* Renders an OLEDDisplay framebuffer with realistic OLED visual effects
* including pixel glow, color tinting, and various display sizes.
*/
import { ref, computed, watch, onMounted, onUnmounted, type PropType } from 'vue';
import { OLEDDisplay } from './OLEDDisplay';
import { DISPLAY_PRESETS, type DisplayPreset } from './DisplayPresets';
const props = defineProps({
/**
* The OLEDDisplay instance to render
*/
display: {
type: Object as PropType<OLEDDisplay>,
required: true,
},
/**
* Pixel scale factor (how many canvas pixels per OLED pixel)
*/
pixelScale: {
type: Number,
default: 4,
},
/**
* Display preset name or custom preset object
*/
preset: {
type: [String, Object] as PropType<string | DisplayPreset>,
default: 'ssd1306-blue',
},
/**
* Enable pixel glow effect (slight bloom around lit pixels)
*/
enableGlow: {
type: Boolean,
default: true,
},
/**
* Enable sub-pixel rendering for more realistic look
*/
enableSubPixel: {
type: Boolean,
default: false,
},
/**
* Show pixel grid lines
*/
showGrid: {
type: Boolean,
default: false,
},
/**
* Auto-refresh interval in ms (0 = manual refresh only)
*/
refreshInterval: {
type: Number,
default: 50,
},
/**
* Yellow/blue split mode for dual-color OLEDs (rows 0-15 yellow)
*/
dualColor: {
type: Boolean,
default: false,
},
});
const emit = defineEmits<{
(e: 'click', x: number, y: number): void;
(e: 'refresh'): void;
}>();
const canvas = ref<HTMLCanvasElement | null>(null);
const refreshTimer = ref<number | null>(null);
// Get the current display preset
const currentPreset = computed<DisplayPreset>(() => {
if (typeof props.preset === 'string') {
return DISPLAY_PRESETS[props.preset] || DISPLAY_PRESETS['ssd1306-blue'];
}
return props.preset;
});
// Canvas dimensions
const canvasWidth = computed(() => props.display.width * props.pixelScale);
const canvasHeight = computed(() => props.display.height * props.pixelScale);
/**
* Render the framebuffer to canvas with OLED effects
*/
function render(): void {
const cvs = canvas.value;
if (!cvs) return;
const ctx = cvs.getContext('2d');
if (!ctx) return;
const buffer = props.display.getBuffer();
const width = props.display.width;
const height = props.display.height;
const scale = props.pixelScale;
const preset = currentPreset.value;
// Create image data
const imageData = ctx.createImageData(canvasWidth.value, canvasHeight.value);
const data = imageData.data;
// Yellow color for dual-color mode (top 16 rows)
const yellowOn = { r: 255, g: 220, b: 50 };
const blueOn = { r: 100, g: 180, b: 255 };
// Render each OLED pixel
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// Get pixel from framebuffer (vertical byte packing)
const byteIndex = x + Math.floor(y / 8) * width;
const bitMask = 1 << (y & 7);
const isOn = (buffer[byteIndex] & bitMask) !== 0;
// Determine pixel color
let pixelColor: { r: number; g: number; b: number };
if (isOn) {
if (props.dualColor && y < 16) {
pixelColor = yellowOn;
} else if (props.dualColor) {
pixelColor = blueOn;
} else {
pixelColor = preset.pixelOn;
}
} else {
pixelColor = preset.pixelOff;
}
// Apply contrast
pixelColor = {
r: Math.min(255, pixelColor.r * preset.contrast),
g: Math.min(255, pixelColor.g * preset.contrast),
b: Math.min(255, pixelColor.b * preset.contrast),
};
// Scale up pixel to canvas
for (let sy = 0; sy < scale; sy++) {
for (let sx = 0; sx < scale; sx++) {
const px = x * scale + sx;
const py = y * scale + sy;
const idx = (py * canvasWidth.value + px) * 4;
// Apply glow effect (brighter center, dimmer edges)
let intensity = 1.0;
if (props.enableGlow && isOn) {
const centerX = scale / 2;
const centerY = scale / 2;
const dist = Math.sqrt(
Math.pow(sx - centerX, 2) + Math.pow(sy - centerY, 2)
);
const maxDist = Math.sqrt(2) * (scale / 2);
intensity = 1.0 - (dist / maxDist) * preset.glow;
}
// Apply sub-pixel rendering (RGB stripes)
if (props.enableSubPixel && isOn) {
const subPixelPos = sx % 3;
if (subPixelPos === 0) {
data[idx] = pixelColor.r * intensity * 1.2;
data[idx + 1] = pixelColor.g * intensity * 0.8;
data[idx + 2] = pixelColor.b * intensity * 0.8;
} else if (subPixelPos === 1) {
data[idx] = pixelColor.r * intensity * 0.8;
data[idx + 1] = pixelColor.g * intensity * 1.2;
data[idx + 2] = pixelColor.b * intensity * 0.8;
} else {
data[idx] = pixelColor.r * intensity * 0.8;
data[idx + 1] = pixelColor.g * intensity * 0.8;
data[idx + 2] = pixelColor.b * intensity * 1.2;
}
} else {
data[idx] = pixelColor.r * intensity;
data[idx + 1] = pixelColor.g * intensity;
data[idx + 2] = pixelColor.b * intensity;
}
data[idx + 3] = 255; // Alpha
}
}
}
}
// Draw grid lines if enabled
if (props.showGrid) {
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// Draw right and bottom edge of each pixel
// Right edge
const rx = (x + 1) * scale - 1;
for (let sy = 0; sy < scale; sy++) {
const py = y * scale + sy;
const idx = (py * canvasWidth.value + rx) * 4;
data[idx] = Math.min(255, data[idx] + 20);
data[idx + 1] = Math.min(255, data[idx + 1] + 20);
data[idx + 2] = Math.min(255, data[idx + 2] + 20);
}
// Bottom edge
const by = (y + 1) * scale - 1;
for (let sx = 0; sx < scale; sx++) {
const px = x * scale + sx;
const idx = (by * canvasWidth.value + px) * 4;
data[idx] = Math.min(255, data[idx] + 20);
data[idx + 1] = Math.min(255, data[idx + 1] + 20);
data[idx + 2] = Math.min(255, data[idx + 2] + 20);
}
}
}
}
ctx.putImageData(imageData, 0, 0);
emit('refresh');
}
/**
* Handle canvas click
*/
function handleClick(event: MouseEvent): void {
const cvs = canvas.value;
if (!cvs) return;
const rect = cvs.getBoundingClientRect();
const scaleX = cvs.width / rect.width;
const scaleY = cvs.height / rect.height;
const canvasX = (event.clientX - rect.left) * scaleX;
const canvasY = (event.clientY - rect.top) * scaleY;
const oledX = Math.floor(canvasX / props.pixelScale);
const oledY = Math.floor(canvasY / props.pixelScale);
emit('click', oledX, oledY);
}
/**
* Start auto-refresh timer
*/
function startRefreshTimer(): void {
stopRefreshTimer();
if (props.refreshInterval > 0) {
refreshTimer.value = window.setInterval(render, props.refreshInterval);
}
}
/**
* Stop auto-refresh timer
*/
function stopRefreshTimer(): void {
if (refreshTimer.value !== null) {
clearInterval(refreshTimer.value);
refreshTimer.value = null;
}
}
// Lifecycle
onMounted(() => {
render();
startRefreshTimer();
});
onUnmounted(() => {
stopRefreshTimer();
});
// Watch for changes
watch(() => props.refreshInterval, startRefreshTimer);
watch(() => props.display, render, { deep: true });
watch(() => props.pixelScale, render);
watch(() => props.preset, render);
watch(() => props.enableGlow, render);
watch(() => props.showGrid, render);
watch(() => props.dualColor, render);
// Expose render method for manual refresh
defineExpose({ render });
</script>
<template>
<div class="oled-emulator-wrapper">
<canvas
ref="canvas"
:width="canvasWidth"
:height="canvasHeight"
class="oled-canvas"
@click="handleClick"
/>
<div class="oled-bezel" v-if="$slots.bezel">
<slot name="bezel" />
</div>
</div>
</template>
<style scoped>
.oled-emulator-wrapper {
display: inline-block;
position: relative;
background: #1a1a1a;
padding: 8px;
border-radius: 6px;
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.oled-canvas {
display: block;
border-radius: 2px;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.oled-bezel {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
</style>
+467
View File
@@ -0,0 +1,467 @@
/**
* OLEDFonts.ts - Font data for OLED display emulator
*
* These are simplified bitmap fonts matching the style used in
* Meshtastic firmware (based on ArialMT from esp8266-oled-ssd1306).
*
* Font format: Vertical byte packing, column-major order
* Each character is drawn column by column, with each byte representing
* 8 vertical pixels.
*/
import type { OLEDFont } from "./OLEDDisplay";
/**
* ArialMT Plain 10 - Small font (height ~13 pixels)
* Covers ASCII 32-126 (space through ~)
*/
export const ArialMT_Plain_10: OLEDFont = {
height: 13,
firstChar: 32,
charCount: 95,
widths: [
3, // space
3, // !
4, // "
6, // #
6, // $
9, // %
7, // &
2, // '
4, // (
4, // )
5, // *
6, // +
3, // ,
4, // -
3, // .
4, // /
6, // 0
6, // 1
6, // 2
6, // 3
6, // 4
6, // 5
6, // 6
6, // 7
6, // 8
6, // 9
3, // :
3, // ;
6, // <
6, // =
6, // >
6, // ?
11, // @
8, // A
7, // B
7, // C
7, // D
6, // E
6, // F
8, // G
7, // H
3, // I
5, // J
7, // K
6, // L
9, // M
7, // N
8, // O
6, // P
8, // Q
7, // R
6, // S
6, // T
7, // U
7, // V
11, // W
7, // X
7, // Y
6, // Z
4, // [
4, // \
4, // ]
6, // ^
6, // _
4, // `
6, // a
6, // b
5, // c
6, // d
6, // e
4, // f
6, // g
6, // h
3, // i
3, // j
6, // k
3, // l
9, // m
6, // n
6, // o
6, // p
6, // q
4, // r
5, // s
4, // t
6, // u
6, // v
9, // w
6, // x
6, // y
5, // z
4, // {
3, // |
4, // }
6, // ~
],
data: new Uint8Array([
// Space (32)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// ! (33)
0x00, 0x00, 0xbe, 0x00, 0x00, 0x00,
// " (34)
0x00, 0x0e, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00,
// # (35)
0x00, 0x48, 0xf8, 0x4e, 0xf8, 0x4e, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00,
// $ (36)
0x00, 0x9c, 0x92, 0xff, 0x92, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// % (37)
0x0c, 0x12, 0x12, 0x8c, 0x60, 0x18, 0xc6, 0x20, 0x20, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// & (38)
0x00, 0x60, 0x9c, 0x92, 0x6a, 0xa4, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// ' (39)
0x00, 0x0e, 0x00, 0x00,
// ( (40)
0x00, 0x7c, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
// ) (41)
0x00, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
// * (42)
0x14, 0x08, 0x3e, 0x08, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00,
// + (43)
0x10, 0x10, 0x7c, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// , (44)
0x00, 0x00, 0xc0, 0x00, 0x00, 0x00,
// - (45)
0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
// . (46)
0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
// / (47)
0x00, 0xc0, 0x38, 0x06, 0x00, 0x00, 0x00, 0x00,
// 0 (48)
0x7c, 0x82, 0x82, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 1 (49)
0x00, 0x04, 0x02, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 2 (50)
0x84, 0xc2, 0xa2, 0x92, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 3 (51)
0x44, 0x82, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 4 (52)
0x30, 0x28, 0x24, 0xfe, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 5 (53)
0x4e, 0x8a, 0x8a, 0x8a, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 6 (54)
0x78, 0x94, 0x92, 0x92, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 7 (55)
0x02, 0xc2, 0x32, 0x0a, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 8 (56)
0x6c, 0x92, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 9 (57)
0x0c, 0x92, 0x92, 0x52, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// : (58)
0x00, 0x00, 0x88, 0x00, 0x00, 0x00,
// ; (59)
0x00, 0x00, 0xc8, 0x00, 0x00, 0x00,
// < (60)
0x10, 0x28, 0x28, 0x44, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// = (61)
0x28, 0x28, 0x28, 0x28, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// > (62)
0x44, 0x44, 0x28, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// ? (63)
0x04, 0x02, 0xa2, 0x12, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// @ (64)
0xf0, 0x08, 0xe4, 0x12, 0x12, 0x12, 0xe2, 0x14, 0x08, 0xf0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// A (65)
0x00, 0xc0, 0x38, 0x26, 0x26, 0x38, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
// B (66)
0xfe, 0x92, 0x92, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// C (67)
0x7c, 0x82, 0x82, 0x82, 0x82, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// D (68)
0xfe, 0x82, 0x82, 0x82, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// E (69)
0xfe, 0x92, 0x92, 0x92, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// F (70)
0xfe, 0x12, 0x12, 0x12, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// G (71)
0x7c, 0x82, 0x82, 0x92, 0x92, 0x74, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
// H (72)
0xfe, 0x10, 0x10, 0x10, 0x10, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// I (73)
0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
// J (74)
0x40, 0x80, 0x80, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// K (75)
0xfe, 0x10, 0x28, 0x44, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// L (76)
0xfe, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// M (77)
0xfe, 0x04, 0x18, 0x60, 0x60, 0x18, 0x04, 0xfe, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// N (78)
0xfe, 0x04, 0x08, 0x30, 0x40, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// O (79)
0x7c, 0x82, 0x82, 0x82, 0x82, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
// P (80)
0xfe, 0x12, 0x12, 0x12, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Q (81)
0x7c, 0x82, 0x82, 0x82, 0x42, 0xc2, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
// R (82)
0xfe, 0x12, 0x12, 0x32, 0x4c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// S (83)
0x4c, 0x92, 0x92, 0x92, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// T (84)
0x02, 0x02, 0xfe, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// U (85)
0x7e, 0x80, 0x80, 0x80, 0x80, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// V (86)
0x06, 0x18, 0x60, 0x80, 0x60, 0x18, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// W (87)
0x06, 0x38, 0xc0, 0x38, 0x06, 0x38, 0xc0, 0x38, 0x06, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// X (88)
0x82, 0x44, 0x28, 0x10, 0x28, 0x44, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// Y (89)
0x02, 0x04, 0x08, 0xf0, 0x08, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
// Z (90)
0xc2, 0xa2, 0x92, 0x8a, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// [ (91)
0x00, 0xfe, 0x82, 0x82, 0x00, 0x00, 0x00, 0x00,
// \ (92)
0x06, 0x38, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
// ] (93)
0x82, 0x82, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00,
// ^ (94)
0x08, 0x04, 0x02, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// _ (95)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// ` (96)
0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
// a (97)
0x40, 0xa8, 0xa8, 0xa8, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// b (98)
0xfe, 0x88, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// c (99)
0x70, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// d (100)
0x70, 0x88, 0x88, 0x88, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// e (101)
0x70, 0xa8, 0xa8, 0xa8, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// f (102)
0x08, 0xfc, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00,
// g (103)
0x30, 0x48, 0x48, 0x48, 0xf8, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00,
// h (104)
0xfe, 0x08, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// i (105)
0x00, 0xfa, 0x00, 0x00, 0x00, 0x00,
// j (106)
0x00, 0x00, 0xfa, 0x01, 0x00, 0x00,
// k (107)
0xfe, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// l (108)
0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
// m (109)
0xf8, 0x08, 0x08, 0xf0, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// n (110)
0xf8, 0x08, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// o (111)
0x70, 0x88, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// p (112)
0xf8, 0x48, 0x48, 0x48, 0x30, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// q (113)
0x30, 0x48, 0x48, 0x48, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
// r (114)
0xf8, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
// s (115)
0x90, 0xa8, 0xa8, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// t (116)
0x08, 0x7e, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00,
// u (117)
0x78, 0x80, 0x80, 0x80, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// v (118)
0x18, 0x60, 0x80, 0x60, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// w (119)
0x78, 0x80, 0x60, 0x18, 0x60, 0x80, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// x (120)
0x88, 0x50, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// y (121)
0x18, 0x60, 0x80, 0x60, 0x18, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
// z (122)
0xc8, 0xa8, 0x98, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// { (123)
0x10, 0x6c, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
// | (124)
0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
// } (125)
0x82, 0x6c, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
// ~ (126)
0x10, 0x08, 0x10, 0x20, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]),
};
/**
* ArialMT Plain 16 - Medium font (height ~19 pixels)
* Simplified subset covering 0-9, A-Z, a-z, common punctuation
*/
export const ArialMT_Plain_16: OLEDFont = {
height: 19,
firstChar: 32,
charCount: 95,
widths: [
4, // space
4, // !
5, // "
8, // #
8, // $
12, // %
10, // &
2, // '
5, // (
5, // )
6, // *
8, // +
4, // ,
5, // -
4, // .
4, // /
8, // 0
8, // 1
8, // 2
8, // 3
8, // 4
8, // 5
8, // 6
8, // 7
8, // 8
8, // 9
4, // :
4, // ;
8, // <
8, // =
8, // >
8, // ?
15, // @
10, // A
9, // B
10, // C
10, // D
9, // E
8, // F
11, // G
9, // H
4, // I
6, // J
9, // K
8, // L
11, // M
9, // N
11, // O
9, // P
11, // Q
10, // R
9, // S
8, // T
9, // U
10, // V
14, // W
10, // X
10, // Y
9, // Z
4, // [
4, // \
4, // ]
7, // ^
8, // _
5, // `
8, // a
8, // b
7, // c
8, // d
8, // e
4, // f
8, // g
8, // h
4, // i
4, // j
8, // k
4, // l
12, // m
8, // n
8, // o
8, // p
8, // q
5, // r
7, // s
4, // t
8, // u
8, // v
12, // w
8, // x
8, // y
7, // z
5, // {
4, // |
5, // }
8, // ~
],
// Font data would be similar pattern but larger - using placeholder
data: new Uint8Array(2500).fill(0xff), // Placeholder - would contain actual font bitmap data
};
/**
* Helper to get FONT_HEIGHT_SMALL equivalent
*/
export const FONT_HEIGHT_SMALL = 13;
export const FONT_HEIGHT_MEDIUM = 19;
export const FONT_HEIGHT_LARGE = 28;
/**
* Helper function to create a font from raw byte data (for importing firmware fonts)
*/
export function createFontFromBytes(
height: number,
firstChar: number,
widths: number[],
data: number[],
): OLEDFont {
return {
height,
firstChar,
charCount: widths.length,
widths,
data: new Uint8Array(data),
};
}
+255
View File
@@ -0,0 +1,255 @@
/**
* OLEDImages.ts - Icon and image data for OLED display emulator
*
* These are direct ports of the XBM/bitmap icons from the Meshtastic firmware.
* Format: XBM style (LSB first, horizontal scanning)
*/
// Satellite icon (8x8)
export const imgSatellite_width = 8;
export const imgSatellite_height = 8;
export const imgSatellite = new Uint8Array([
0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b11011011, 0b11111111,
0b11011011, 0b00011000,
]);
// USB icon (10x8)
export const imgUSB = new Uint8Array([
0x00, 0xfc, 0xf0, 0xfc, 0x88, 0xff, 0x86, 0xfe, 0x85, 0xfe, 0x89, 0xff, 0xf1,
0xfc, 0x00, 0xfc,
]);
// Power icon (8x16)
export const imgPower = new Uint8Array([
0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, 0x1c, 0x22, 0x22, 0x41, 0x7f,
0x22, 0x22, 0x22,
]);
// User icon (8x8)
export const imgUser = new Uint8Array([
0x3c, 0x42, 0x99, 0xa5, 0xa5, 0x99, 0x42, 0x3c,
]);
// Position empty arrow (6x8)
export const imgPositionEmpty = new Uint8Array([
0x20, 0x30, 0x28, 0x24, 0x42, 0xff,
]);
// Position solid arrow (6x8)
export const imgPositionSolid = new Uint8Array([
0x20, 0x30, 0x38, 0x3c, 0x7e, 0xff,
]);
// Mail icon (10x7)
export const mail_width = 10;
export const mail_height = 7;
export const mail = new Uint8Array([
0b11111111,
0b00, // Top line
0b10000001,
0b00, // Edges
0b11000011,
0b00, // Diagonals start
0b10100101,
0b00, // Inner M part
0b10011001,
0b00, // Inner M part
0b10000001,
0b00, // Edges
0b11111111,
0b00, // Bottom line
]);
// Hop icon (9x10)
export const hop_width = 9;
export const hop_height = 10;
export const hop = new Uint8Array([
0x05, 0x00, 0x07, 0x00, 0x05, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, 0x00, 0xc0,
0x01, 0x40, 0x01, 0xc0, 0x01, 0x40, 0x00,
]);
// Mail icon (8x8)
export const icon_mail = new Uint8Array([
0b11111111, // ████████ top border
0b10000001, // █ █ sides
0b11000011, // ██ ██ diagonal
0b10100101, // █ █ █ █ inner M
0b10011001, // █ ██ █ inner M
0b10000001, // █ █ sides
0b10000001, // █ █ sides
0b11111111, // ████████ bottom
]);
// Compass icon (8x8)
export const icon_compass = new Uint8Array([
0x3c, // Row 0: ..####..
0x52, // Row 1: .#..#.#.
0x91, // Row 2: #...#..#
0x91, // Row 3: #...#..#
0x91, // Row 4: #...#..#
0x81, // Row 5: #......#
0x42, // Row 6: .#....#.
0x3c, // Row 7: ..####..
]);
// Radio icon (8x8)
export const icon_radio = new Uint8Array([
0x0f, // Row 0: ####....
0x10, // Row 1: ....#...
0x27, // Row 2: ###..#..
0x48, // Row 3: ...#..#.
0x93, // Row 4: ##..#..#
0xa4, // Row 5: ..#..#.#
0xa8, // Row 6: ...#.#.#
0xa9, // Row 7: #..#.#.#
]);
// System/settings icon (8x8)
export const icon_system = new Uint8Array([
0x24, // Row 0: ..#..#..
0x3c, // Row 1: ..####..
0xc3, // Row 2: ##....##
0x5a, // Row 3: .#.##.#.
0x5a, // Row 4: .#.##.#.
0xc3, // Row 5: ##....##
0x3c, // Row 6: ..####..
0x24, // Row 7: ..#..#..
]);
// WiFi icon (8x8)
export const icon_wifi = new Uint8Array([
0b00000000, 0b00011000, 0b00111100, 0b01111110, 0b11011011, 0b00011000,
0b00011000, 0b00000000,
]);
// Nodes icon (8x8)
export const icon_nodes = new Uint8Array([
0xf9, // Row 0: #..#####
0x00, // Row 1
0xf9, // Row 2: #..#####
0x00, // Row 3
0xf9, // Row 4: #..#####
0x00, // Row 5
0xf9, // Row 6: #..#####
0x00, // Row 7
]);
// Battery vertical (7x11)
export const batteryBitmap_v = new Uint8Array([
0b00011100, 0b00111110, 0b01000001, 0b01000001, 0b00000000, 0b00000000,
0b00000000, 0b01000001, 0b01000001, 0b01000001, 0b00111110,
]);
// Battery side gaps (8x3)
export const batteryBitmap_sidegaps_v = new Uint8Array([
0b10000010, 0b10000010, 0b10000010,
]);
// Lightning bolt vertical (5x5)
export const lightning_bolt_v = new Uint8Array([
0b00000100, 0b00000110, 0b00011111, 0b00001100, 0b00000100,
]);
// Horizontal battery bottom (13x9)
export const batteryBitmap_h_bottom = new Uint8Array([
0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
0b00011110, 0b00000000,
]);
// Horizontal battery top (13x9)
export const batteryBitmap_h_top = new Uint8Array([
0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000,
0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b11000000, 0b00000000,
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000,
0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000,
0b00111100, 0b00000000,
]);
// Lightning bolt horizontal (13x9)
export const lightning_bolt_h = new Uint8Array([
0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000,
0b00111000, 0b00000000, 0b00111100, 0b00000000, 0b00011110, 0b00000000,
0b11111111, 0b00000000, 0b01111000, 0b00000000, 0b00111100, 0b00000000,
0b00011100, 0b00000000, 0b00001100, 0b00000000, 0b00000100, 0b00000000,
0b00000000, 0b00000000,
]);
// Signal bars icon (various signal strengths)
export const signal_bars = {
width: 12,
height: 8,
// 0 bars
none: new Uint8Array([
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b00000000, 0b00000000,
]),
// 1 bar
low: new Uint8Array([
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000011, 0b00000000, 0b00000011, 0b00000000,
]),
// 2 bars
medium: new Uint8Array([
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b00001100, 0b00000000, 0b00001100, 0b00000000,
0b00001111, 0b00000000, 0b00001111, 0b00000000,
]),
// 3 bars
high: new Uint8Array([
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00110000, 0b00000000,
0b00110000, 0b00000000, 0b00111100, 0b00000000, 0b00111100, 0b00000000,
0b00111111, 0b00000000, 0b00111111, 0b00000000,
]),
// 4 bars (full)
full: new Uint8Array([
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11110000, 0b00000000,
0b11110000, 0b00000000, 0b11111100, 0b00000000, 0b11111100, 0b00000000,
0b11111111, 0b00000000, 0b11111111, 0b00000000,
]),
};
// Meshtastic logo (simplified 32x32)
export const logo_width = 32;
export const logo_height = 32;
export const meshtastic_logo = new Uint8Array([
0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00,
0x03, 0xc0, 0x00, 0x80, 0x01, 0x80, 0x01, 0xc0, 0x00, 0x00, 0x03, 0x60, 0x00,
0x00, 0x06, 0x30, 0x00, 0x00, 0x0c, 0x18, 0xf8, 0x1f, 0x18, 0x08, 0x04, 0x20,
0x10, 0x0c, 0x02, 0x40, 0x30, 0x04, 0x01, 0x80, 0x20, 0x06, 0x01, 0x80, 0x60,
0x02, 0xe1, 0x87, 0x40, 0x02, 0x11, 0x88, 0x40, 0x03, 0x09, 0x90, 0xc0, 0x03,
0x09, 0x90, 0xc0, 0x02, 0x11, 0x88, 0x40, 0x02, 0xe1, 0x87, 0x40, 0x06, 0x01,
0x80, 0x60, 0x04, 0x01, 0x80, 0x20, 0x0c, 0x02, 0x40, 0x30, 0x08, 0x04, 0x20,
0x10, 0x18, 0xf8, 0x1f, 0x18, 0x30, 0x00, 0x00, 0x0c, 0x60, 0x00, 0x00, 0x06,
0xc0, 0x00, 0x00, 0x03, 0x80, 0x01, 0x80, 0x01, 0x00, 0x03, 0xc0, 0x00, 0x00,
0x06, 0x60, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00,
]);
// Bluetooth icon (8x11)
export const icon_bluetooth = new Uint8Array([
0b00000000, 0b10000100, 0b01001010, 0b00101010, 0b00010100, 0b11111111,
0b00010100, 0b00101010, 0b01001010, 0b10000100, 0b00000000,
]);
// GPS/Location pin icon (8x12)
export const icon_gps_pin = new Uint8Array([
0b00111100, 0b01111110, 0b11111111, 0b11100111, 0b11100111, 0b11111111,
0b01111110, 0b00111100, 0b00011000, 0b00011000, 0b00001000, 0b00000000,
]);
// Message/chat bubble icon (10x8)
export const icon_message = new Uint8Array([
0b11111111, 0b00, 0b10000001, 0b00, 0b10000001, 0b00, 0b10000001, 0b00,
0b10000001, 0b00, 0b11111111, 0b00, 0b00000110, 0b00, 0b00000010, 0b00,
]);
/**
* Helper to convert icon data to XBM format if needed
*/
export function toXbm(data: Uint8Array | number[]): Uint8Array {
return data instanceof Uint8Array ? data : new Uint8Array(data);
}
+627
View File
@@ -0,0 +1,627 @@
/**
* ScreenRenderers.ts - Sample screen rendering functions
*
* These demonstrate how to port Meshtastic firmware UI screens
* to the OLED emulator. The API matches the firmware's drawing code.
*/
import {
OLEDDisplay,
WHITE,
BLACK,
TEXT_ALIGN_LEFT,
TEXT_ALIGN_CENTER,
TEXT_ALIGN_RIGHT,
} from "./OLEDDisplay";
import { Font_6x8 } from "./SimpleFonts";
import * as images from "./OLEDImages";
// Font height for simple 6x8 font
const FONT_HEIGHT_SMALL = 8;
// Screen resolution detection (matches SharedUIDisplay.cpp)
export type ScreenResolution = "UltraLow" | "Low" | "High";
export function determineScreenResolution(
width: number,
height: number,
): ScreenResolution {
if (width <= 64 || height <= 48) {
return "UltraLow";
}
if (width > 128) {
return "High";
}
return "Low";
}
// Helper for consistent line positioning
export function getTextPositions(display: OLEDDisplay): number[] {
const fontHeight = FONT_HEIGHT_SMALL;
return [
0, // textZeroLine
fontHeight - 1, // textFirstLine
fontHeight - 1 + (fontHeight - 5), // textSecondLine
fontHeight - 1 + 2 * (fontHeight - 5), // textThirdLine
fontHeight - 1 + 3 * (fontHeight - 5), // textFourthLine
fontHeight - 1 + 4 * (fontHeight - 5), // textFifthLine
];
}
/**
* Draw a rounded highlight box (used for inverted headers)
*/
export function drawRoundedHighlight(
display: OLEDDisplay,
x: number,
y: number,
w: number,
h: number,
r: number,
): void {
// Draw the center and side rectangles
display.fillRect(x + r, y, w - 2 * r, h); // center bar
display.fillRect(x, y + r, r, h - 2 * r); // left edge
display.fillRect(x + w - r, y + r, r, h - 2 * r); // right edge
// Draw the rounded corners using filled circles
display.fillCircle(x + r + 1, y + r, r); // top-left
display.fillCircle(x + w - r - 1, y + r, r); // top-right
display.fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left
display.fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right
}
/**
* Draw the common header bar (battery, title, icons)
*/
export function drawCommonHeader(
display: OLEDDisplay,
x: number,
y: number,
titleStr: string,
options: {
inverted?: boolean;
batteryPercent?: number;
isCharging?: boolean;
hasUSB?: boolean;
hasUnreadMessage?: boolean;
} = {},
): void {
const {
inverted = true,
batteryPercent = 75,
isCharging = false,
hasUSB = false,
hasUnreadMessage = false,
} = options;
const HEADER_OFFSET_Y = 1;
y += HEADER_OFFSET_Y;
display.setFont(Font_6x8);
const highlightHeight = FONT_HEIGHT_SMALL - 1;
const screenW = display.getWidth();
if (inverted) {
// Draw inverted header background
display.setColor(WHITE);
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
display.setColor(BLACK);
} else {
// Draw line under header
display.setColor(WHITE);
display.drawLine(0, 14, screenW, 14);
}
// Draw title centered
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(screenW / 2, y, titleStr);
// Reset color for remaining elements
if (inverted) {
display.setColor(WHITE);
}
// Draw battery on left
let batteryX = 1;
const batteryY = HEADER_OFFSET_Y + 1;
if (hasUSB && !isCharging) {
// Draw USB icon
display.drawXbm(batteryX + 1, batteryY + 2, 10, 8, images.imgUSB);
batteryX += 11;
} else {
// Draw battery outline
display.drawXbm(batteryX, batteryY, 7, 11, images.batteryBitmap_v);
if (isCharging) {
// Draw lightning bolt
display.drawXbm(
batteryX + 1,
batteryY + 3,
5,
5,
images.lightning_bolt_v,
);
} else {
// Draw battery level
display.drawXbm(
batteryX - 1,
batteryY + 4,
8,
3,
images.batteryBitmap_sidegaps_v,
);
const fillHeight = Math.floor((8 * batteryPercent) / 100);
const fillY = batteryY - fillHeight + 10;
display.fillRect(batteryX + 1, fillY, 5, fillHeight);
}
batteryX += 9;
}
// Draw mail icon on right if unread message
if (hasUnreadMessage) {
const mailX = screenW - images.mail_width - 2;
display.drawXbm(
mailX,
y + 2,
images.mail_width,
images.mail_height,
images.mail,
);
}
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.setColor(WHITE);
}
/**
* Draw a boot/splash screen with logo
*/
export function drawBootScreen(
display: OLEDDisplay,
appVersion: string = "2.5.0",
): void {
display.clear();
display.setFont(Font_6x8);
display.setColor(WHITE);
const centerX = display.getWidth() / 2;
const centerY = display.getHeight() / 2;
// Draw logo centered
const logoX = centerX - images.logo_width / 2;
const logoY = centerY - images.logo_height / 2 - 8;
display.drawXbm(
logoX,
logoY,
images.logo_width,
images.logo_height,
images.meshtastic_logo,
);
// Draw version below
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(
centerX,
centerY + images.logo_height / 2,
`v${appVersion}`,
);
display.setTextAlignment(TEXT_ALIGN_LEFT);
}
/**
* Draw a node info screen
*/
export function drawNodeInfoScreen(
display: OLEDDisplay,
nodeInfo: {
shortName: string;
longName: string;
nodeId: string;
batteryLevel?: number;
lastHeard?: string;
snr?: number;
hopsAway?: number;
},
): void {
display.clear();
// Draw header
drawCommonHeader(display, 0, 0, "Node Info", {
inverted: true,
batteryPercent: 85,
});
display.setFont(Font_6x8);
display.setColor(WHITE);
display.setTextAlignment(TEXT_ALIGN_LEFT);
const lines = getTextPositions(display);
const screenW = display.getWidth();
// Node name (bolded by drawing twice offset)
display.drawString(0, lines[1], nodeInfo.longName);
display.drawString(1, lines[1], nodeInfo.longName);
// Short name and ID
display.drawString(0, lines[2], `${nodeInfo.shortName}${nodeInfo.nodeId}`);
// Last heard
if (nodeInfo.lastHeard) {
display.drawString(0, lines[3], `Heard: ${nodeInfo.lastHeard}`);
}
// Signal info on right side
if (nodeInfo.snr !== undefined) {
const snrStr = `SNR: ${nodeInfo.snr}dB`;
display.setTextAlignment(TEXT_ALIGN_RIGHT);
display.drawString(screenW, lines[3], snrStr);
}
// Hops
if (nodeInfo.hopsAway !== undefined) {
const hopStr =
nodeInfo.hopsAway === 0 ? "Direct" : `${nodeInfo.hopsAway} hops`;
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.drawString(0, lines[4], hopStr);
}
display.setTextAlignment(TEXT_ALIGN_LEFT);
}
/**
* Draw a message screen
*/
export function drawMessageScreen(
display: OLEDDisplay,
message: {
from: string;
text: string;
time: string;
channel?: string;
},
): void {
display.clear();
// Draw header with channel name if provided
const headerTitle = message.channel || "Message";
drawCommonHeader(display, 0, 0, headerTitle, {
inverted: true,
batteryPercent: 72,
hasUnreadMessage: true,
});
display.setFont(Font_6x8);
display.setColor(WHITE);
display.setTextAlignment(TEXT_ALIGN_LEFT);
const lines = getTextPositions(display);
const screenW = display.getWidth();
// From line
display.drawString(0, lines[1], `From: ${message.from}`);
// Time on right
display.setTextAlignment(TEXT_ALIGN_RIGHT);
display.drawString(screenW, lines[1], message.time);
// Message text (with word wrap)
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.drawStringMaxWidth(0, lines[2], screenW, message.text);
}
/**
* Draw a GPS/location screen
*/
export function drawGPSScreen(
display: OLEDDisplay,
gpsInfo: {
latitude?: number;
longitude?: number;
altitude?: number;
satellites?: number;
hasLock: boolean;
speed?: number;
},
): void {
display.clear();
drawCommonHeader(display, 0, 0, "GPS", {
inverted: true,
batteryPercent: 90,
});
display.setFont(Font_6x8);
display.setColor(WHITE);
display.setTextAlignment(TEXT_ALIGN_LEFT);
const lines = getTextPositions(display);
// Draw satellite icon and count
display.drawXbm(
0,
lines[1],
images.imgSatellite_width,
images.imgSatellite_height,
images.imgSatellite,
);
if (!gpsInfo.hasLock) {
display.drawString(12, lines[1], "No Lock");
} else {
display.drawString(12, lines[1], `${gpsInfo.satellites || 0} sats`);
}
if (
gpsInfo.hasLock &&
gpsInfo.latitude !== undefined &&
gpsInfo.longitude !== undefined
) {
// Coordinates
display.drawString(0, lines[2], `Lat: ${gpsInfo.latitude.toFixed(6)}`);
display.drawString(0, lines[3], `Lon: ${gpsInfo.longitude.toFixed(6)}`);
// Altitude
if (gpsInfo.altitude !== undefined) {
display.drawString(0, lines[4], `Alt: ${gpsInfo.altitude.toFixed(0)}m`);
}
// Speed
if (gpsInfo.speed !== undefined) {
const screenW = display.getWidth();
display.setTextAlignment(TEXT_ALIGN_RIGHT);
display.drawString(screenW, lines[4], `${gpsInfo.speed.toFixed(1)} km/h`);
}
}
display.setTextAlignment(TEXT_ALIGN_LEFT);
}
/**
* Draw a node list screen
*/
export function drawNodeListScreen(
display: OLEDDisplay,
nodes: Array<{
shortName: string;
longName: string;
lastHeard: string;
snr?: number;
}>,
selectedIndex: number = 0,
): void {
display.clear();
drawCommonHeader(display, 0, 0, `Nodes (${nodes.length})`, {
inverted: true,
batteryPercent: 80,
});
display.setFont(Font_6x8);
display.setColor(WHITE);
display.setTextAlignment(TEXT_ALIGN_LEFT);
const lines = getTextPositions(display);
const screenW = display.getWidth();
const maxVisibleNodes = 4;
const startIndex = Math.max(
0,
selectedIndex - Math.floor(maxVisibleNodes / 2),
);
for (let i = 0; i < maxVisibleNodes && startIndex + i < nodes.length; i++) {
const node = nodes[startIndex + i];
const lineY = lines[i + 1];
const isSelected = startIndex + i === selectedIndex;
// Highlight selected row
if (isSelected) {
display.fillRect(0, lineY - 1, screenW, FONT_HEIGHT_SMALL);
display.setColor(BLACK);
}
// Node short name
display.drawString(0, lineY, node.shortName);
// Last heard on right
display.setTextAlignment(TEXT_ALIGN_RIGHT);
display.drawString(screenW, lineY, node.lastHeard);
display.setTextAlignment(TEXT_ALIGN_LEFT);
if (isSelected) {
display.setColor(WHITE);
}
}
}
/**
* Draw a system info screen
*/
export function drawSystemScreen(
display: OLEDDisplay,
sysInfo: {
uptime: string;
channelUtil: number;
airUtil: number;
batteryVoltage?: number;
nodes: number;
freeMemory?: number;
},
): void {
display.clear();
drawCommonHeader(display, 0, 0, "System", {
inverted: true,
batteryPercent: 95,
});
display.setFont(Font_6x8);
display.setColor(WHITE);
display.setTextAlignment(TEXT_ALIGN_LEFT);
const lines = getTextPositions(display);
const screenW = display.getWidth();
// Uptime
display.drawString(0, lines[1], `Uptime: ${sysInfo.uptime}`);
// Channel utilization
display.drawString(0, lines[2], `ChUtil: ${sysInfo.channelUtil.toFixed(1)}%`);
// Air utilization
display.setTextAlignment(TEXT_ALIGN_RIGHT);
display.drawString(
screenW,
lines[2],
`AirTx: ${sysInfo.airUtil.toFixed(1)}%`,
);
display.setTextAlignment(TEXT_ALIGN_LEFT);
// Nodes
display.drawString(0, lines[3], `Nodes: ${sysInfo.nodes}`);
// Battery voltage
if (sysInfo.batteryVoltage !== undefined) {
display.setTextAlignment(TEXT_ALIGN_RIGHT);
display.drawString(
screenW,
lines[3],
`${sysInfo.batteryVoltage.toFixed(2)}V`,
);
}
// Free memory
if (sysInfo.freeMemory !== undefined) {
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.drawString(
0,
lines[4],
`Free: ${(sysInfo.freeMemory / 1024).toFixed(0)}KB`,
);
}
display.setTextAlignment(TEXT_ALIGN_LEFT);
}
/**
* Draw a compass screen with bearing arrow
*/
export function drawCompassScreen(
display: OLEDDisplay,
compassInfo: {
heading: number; // Device heading in degrees
bearing: number; // Bearing to target in degrees
distance?: number; // Distance to target in meters
targetName?: string;
},
): void {
display.clear();
drawCommonHeader(display, 0, 0, compassInfo.targetName || "Compass", {
inverted: true,
batteryPercent: 70,
});
display.setFont(Font_6x8);
display.setColor(WHITE);
const screenW = display.getWidth();
const screenH = display.getHeight();
// Compass center and radius
const compassRadius = Math.min(screenW, screenH - 20) / 2 - 4;
const compassX = screenW / 2;
const compassY = (screenH + 16) / 2;
// Draw compass circle
display.drawCircle(compassX, compassY, compassRadius);
// Draw cardinal directions
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(compassX, compassY - compassRadius - 10, "N");
// Calculate relative bearing (bearing - heading)
const relativeBearing =
((compassInfo.bearing - compassInfo.heading) * Math.PI) / 180;
// Draw bearing arrow
const arrowLength = compassRadius - 6;
const arrowX = compassX + Math.sin(relativeBearing) * arrowLength;
const arrowY = compassY - Math.cos(relativeBearing) * arrowLength;
display.drawLine(compassX, compassY, arrowX, arrowY);
// Draw arrowhead
const headAngle = 0.4;
const headLength = 8;
const angle1 = relativeBearing + Math.PI - headAngle;
const angle2 = relativeBearing + Math.PI + headAngle;
display.drawLine(
arrowX,
arrowY,
arrowX + Math.sin(angle1) * headLength,
arrowY - Math.cos(angle1) * headLength,
);
display.drawLine(
arrowX,
arrowY,
arrowX + Math.sin(angle2) * headLength,
arrowY - Math.cos(angle2) * headLength,
);
// Draw distance at bottom
if (compassInfo.distance !== undefined) {
let distStr: string;
if (compassInfo.distance >= 1000) {
distStr = `${(compassInfo.distance / 1000).toFixed(1)} km`;
} else {
distStr = `${Math.round(compassInfo.distance)} m`;
}
display.drawString(compassX, screenH - FONT_HEIGHT_SMALL, distStr);
}
display.setTextAlignment(TEXT_ALIGN_LEFT);
}
/**
* Draw a progress/loading screen
*/
export function drawProgressScreen(
display: OLEDDisplay,
title: string,
progress: number,
statusText?: string,
): void {
display.clear();
display.setFont(Font_6x8);
display.setColor(WHITE);
display.setTextAlignment(TEXT_ALIGN_CENTER);
const screenW = display.getWidth();
const screenH = display.getHeight();
const centerX = screenW / 2;
const centerY = screenH / 2;
// Title
display.drawString(centerX, centerY - 20, title);
// Progress bar
const barWidth = screenW - 20;
const barHeight = 10;
const barX = 10;
const barY = centerY - barHeight / 2;
display.drawProgressBar(barX, barY, barWidth, barHeight, progress);
// Progress percentage
display.drawString(centerX, barY + barHeight + 4, `${Math.round(progress)}%`);
// Status text
if (statusText) {
display.drawString(centerX, screenH - FONT_HEIGHT_SMALL - 2, statusText);
}
display.setTextAlignment(TEXT_ALIGN_LEFT);
}
+421
View File
@@ -0,0 +1,421 @@
/**
* SimpleFonts.ts - Simple working bitmap fonts for OLED emulator
*
* These are basic monospace bitmap fonts that are easy to render correctly.
* Format: Each character is stored in column-major order, 1 byte per column.
* For fonts taller than 8px, multiple bytes per column are used.
*/
import type { OLEDFont } from "./OLEDDisplay";
/**
* Simple 6x8 monospace font
* Each character is 6 columns wide, 8 rows tall
* 1 byte per column (8 vertical pixels)
* Includes ASCII 32-126
*/
export const Font_6x8: OLEDFont = {
height: 8,
firstChar: 32,
charCount: 95,
widths: new Array(95).fill(6), // Fixed width font
data: new Uint8Array([
// Space (32)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// ! (33)
0x00, 0x00, 0x5f, 0x00, 0x00, 0x00,
// " (34)
0x00, 0x07, 0x00, 0x07, 0x00, 0x00,
// # (35)
0x14, 0x7f, 0x14, 0x7f, 0x14, 0x00,
// $ (36)
0x24, 0x2a, 0x7f, 0x2a, 0x12, 0x00,
// % (37)
0x23, 0x13, 0x08, 0x64, 0x62, 0x00,
// & (38)
0x36, 0x49, 0x55, 0x22, 0x50, 0x00,
// ' (39)
0x00, 0x05, 0x03, 0x00, 0x00, 0x00,
// ( (40)
0x00, 0x1c, 0x22, 0x41, 0x00, 0x00,
// ) (41)
0x00, 0x41, 0x22, 0x1c, 0x00, 0x00,
// * (42)
0x08, 0x2a, 0x1c, 0x2a, 0x08, 0x00,
// + (43)
0x08, 0x08, 0x3e, 0x08, 0x08, 0x00,
// , (44)
0x00, 0x50, 0x30, 0x00, 0x00, 0x00,
// - (45)
0x08, 0x08, 0x08, 0x08, 0x08, 0x00,
// . (46)
0x00, 0x60, 0x60, 0x00, 0x00, 0x00,
// / (47)
0x20, 0x10, 0x08, 0x04, 0x02, 0x00,
// 0 (48)
0x3e, 0x51, 0x49, 0x45, 0x3e, 0x00,
// 1 (49)
0x00, 0x42, 0x7f, 0x40, 0x00, 0x00,
// 2 (50)
0x42, 0x61, 0x51, 0x49, 0x46, 0x00,
// 3 (51)
0x21, 0x41, 0x45, 0x4b, 0x31, 0x00,
// 4 (52)
0x18, 0x14, 0x12, 0x7f, 0x10, 0x00,
// 5 (53)
0x27, 0x45, 0x45, 0x45, 0x39, 0x00,
// 6 (54)
0x3c, 0x4a, 0x49, 0x49, 0x30, 0x00,
// 7 (55)
0x01, 0x71, 0x09, 0x05, 0x03, 0x00,
// 8 (56)
0x36, 0x49, 0x49, 0x49, 0x36, 0x00,
// 9 (57)
0x06, 0x49, 0x49, 0x29, 0x1e, 0x00,
// : (58)
0x00, 0x36, 0x36, 0x00, 0x00, 0x00,
// ; (59)
0x00, 0x56, 0x36, 0x00, 0x00, 0x00,
// < (60)
0x00, 0x08, 0x14, 0x22, 0x41, 0x00,
// = (61)
0x14, 0x14, 0x14, 0x14, 0x14, 0x00,
// > (62)
0x41, 0x22, 0x14, 0x08, 0x00, 0x00,
// ? (63)
0x02, 0x01, 0x51, 0x09, 0x06, 0x00,
// @ (64)
0x32, 0x49, 0x79, 0x41, 0x3e, 0x00,
// A (65)
0x7e, 0x11, 0x11, 0x11, 0x7e, 0x00,
// B (66)
0x7f, 0x49, 0x49, 0x49, 0x36, 0x00,
// C (67)
0x3e, 0x41, 0x41, 0x41, 0x22, 0x00,
// D (68)
0x7f, 0x41, 0x41, 0x22, 0x1c, 0x00,
// E (69)
0x7f, 0x49, 0x49, 0x49, 0x41, 0x00,
// F (70)
0x7f, 0x09, 0x09, 0x01, 0x01, 0x00,
// G (71)
0x3e, 0x41, 0x41, 0x51, 0x32, 0x00,
// H (72)
0x7f, 0x08, 0x08, 0x08, 0x7f, 0x00,
// I (73)
0x00, 0x41, 0x7f, 0x41, 0x00, 0x00,
// J (74)
0x20, 0x40, 0x41, 0x3f, 0x01, 0x00,
// K (75)
0x7f, 0x08, 0x14, 0x22, 0x41, 0x00,
// L (76)
0x7f, 0x40, 0x40, 0x40, 0x40, 0x00,
// M (77)
0x7f, 0x02, 0x04, 0x02, 0x7f, 0x00,
// N (78)
0x7f, 0x04, 0x08, 0x10, 0x7f, 0x00,
// O (79)
0x3e, 0x41, 0x41, 0x41, 0x3e, 0x00,
// P (80)
0x7f, 0x09, 0x09, 0x09, 0x06, 0x00,
// Q (81)
0x3e, 0x41, 0x51, 0x21, 0x5e, 0x00,
// R (82)
0x7f, 0x09, 0x19, 0x29, 0x46, 0x00,
// S (83)
0x46, 0x49, 0x49, 0x49, 0x31, 0x00,
// T (84)
0x01, 0x01, 0x7f, 0x01, 0x01, 0x00,
// U (85)
0x3f, 0x40, 0x40, 0x40, 0x3f, 0x00,
// V (86)
0x1f, 0x20, 0x40, 0x20, 0x1f, 0x00,
// W (87)
0x7f, 0x20, 0x18, 0x20, 0x7f, 0x00,
// X (88)
0x63, 0x14, 0x08, 0x14, 0x63, 0x00,
// Y (89)
0x03, 0x04, 0x78, 0x04, 0x03, 0x00,
// Z (90)
0x61, 0x51, 0x49, 0x45, 0x43, 0x00,
// [ (91)
0x00, 0x00, 0x7f, 0x41, 0x41, 0x00,
// \ (92)
0x02, 0x04, 0x08, 0x10, 0x20, 0x00,
// ] (93)
0x41, 0x41, 0x7f, 0x00, 0x00, 0x00,
// ^ (94)
0x04, 0x02, 0x01, 0x02, 0x04, 0x00,
// _ (95)
0x40, 0x40, 0x40, 0x40, 0x40, 0x00,
// ` (96)
0x00, 0x01, 0x02, 0x04, 0x00, 0x00,
// a (97)
0x20, 0x54, 0x54, 0x54, 0x78, 0x00,
// b (98)
0x7f, 0x48, 0x44, 0x44, 0x38, 0x00,
// c (99)
0x38, 0x44, 0x44, 0x44, 0x20, 0x00,
// d (100)
0x38, 0x44, 0x44, 0x48, 0x7f, 0x00,
// e (101)
0x38, 0x54, 0x54, 0x54, 0x18, 0x00,
// f (102)
0x08, 0x7e, 0x09, 0x01, 0x02, 0x00,
// g (103)
0x08, 0x14, 0x54, 0x54, 0x3c, 0x00,
// h (104)
0x7f, 0x08, 0x04, 0x04, 0x78, 0x00,
// i (105)
0x00, 0x44, 0x7d, 0x40, 0x00, 0x00,
// j (106)
0x20, 0x40, 0x44, 0x3d, 0x00, 0x00,
// k (107)
0x00, 0x7f, 0x10, 0x28, 0x44, 0x00,
// l (108)
0x00, 0x41, 0x7f, 0x40, 0x00, 0x00,
// m (109)
0x7c, 0x04, 0x18, 0x04, 0x78, 0x00,
// n (110)
0x7c, 0x08, 0x04, 0x04, 0x78, 0x00,
// o (111)
0x38, 0x44, 0x44, 0x44, 0x38, 0x00,
// p (112)
0x7c, 0x14, 0x14, 0x14, 0x08, 0x00,
// q (113)
0x08, 0x14, 0x14, 0x18, 0x7c, 0x00,
// r (114)
0x7c, 0x08, 0x04, 0x04, 0x08, 0x00,
// s (115)
0x48, 0x54, 0x54, 0x54, 0x20, 0x00,
// t (116)
0x04, 0x3f, 0x44, 0x40, 0x20, 0x00,
// u (117)
0x3c, 0x40, 0x40, 0x20, 0x7c, 0x00,
// v (118)
0x1c, 0x20, 0x40, 0x20, 0x1c, 0x00,
// w (119)
0x3c, 0x40, 0x30, 0x40, 0x3c, 0x00,
// x (120)
0x44, 0x28, 0x10, 0x28, 0x44, 0x00,
// y (121)
0x0c, 0x50, 0x50, 0x50, 0x3c, 0x00,
// z (122)
0x44, 0x64, 0x54, 0x4c, 0x44, 0x00,
// { (123)
0x00, 0x08, 0x36, 0x41, 0x00, 0x00,
// | (124)
0x00, 0x00, 0x7f, 0x00, 0x00, 0x00,
// } (125)
0x00, 0x41, 0x36, 0x08, 0x00, 0x00,
// ~ (126)
0x08, 0x08, 0x2a, 0x1c, 0x08, 0x00,
]),
};
/**
* Simple 5x7 monospace font (more compact)
* Each character is 5 columns wide, 7 rows tall
* 1 byte per column
*/
export const Font_5x7: OLEDFont = {
height: 7,
firstChar: 32,
charCount: 95,
widths: new Array(95).fill(5),
data: new Uint8Array([
// Space (32)
0x00, 0x00, 0x00, 0x00, 0x00,
// ! (33)
0x00, 0x00, 0x2f, 0x00, 0x00,
// " (34)
0x00, 0x07, 0x00, 0x07, 0x00,
// # (35)
0x14, 0x3e, 0x14, 0x3e, 0x14,
// $ (36)
0x24, 0x2a, 0x7f, 0x2a, 0x12,
// % (37)
0x23, 0x13, 0x08, 0x64, 0x62,
// & (38)
0x36, 0x49, 0x55, 0x22, 0x50,
// ' (39)
0x00, 0x05, 0x03, 0x00, 0x00,
// ( (40)
0x00, 0x1c, 0x22, 0x41, 0x00,
// ) (41)
0x00, 0x41, 0x22, 0x1c, 0x00,
// * (42)
0x14, 0x08, 0x3e, 0x08, 0x14,
// + (43)
0x08, 0x08, 0x3e, 0x08, 0x08,
// , (44)
0x00, 0x50, 0x30, 0x00, 0x00,
// - (45)
0x08, 0x08, 0x08, 0x08, 0x08,
// . (46)
0x00, 0x30, 0x30, 0x00, 0x00,
// / (47)
0x20, 0x10, 0x08, 0x04, 0x02,
// 0 (48)
0x3e, 0x51, 0x49, 0x45, 0x3e,
// 1 (49)
0x00, 0x42, 0x7f, 0x40, 0x00,
// 2 (50)
0x42, 0x61, 0x51, 0x49, 0x46,
// 3 (51)
0x21, 0x41, 0x45, 0x4b, 0x31,
// 4 (52)
0x18, 0x14, 0x12, 0x7f, 0x10,
// 5 (53)
0x27, 0x45, 0x45, 0x45, 0x39,
// 6 (54)
0x3c, 0x4a, 0x49, 0x49, 0x30,
// 7 (55)
0x01, 0x71, 0x09, 0x05, 0x03,
// 8 (56)
0x36, 0x49, 0x49, 0x49, 0x36,
// 9 (57)
0x06, 0x49, 0x49, 0x29, 0x1e,
// : (58)
0x00, 0x36, 0x36, 0x00, 0x00,
// ; (59)
0x00, 0x56, 0x36, 0x00, 0x00,
// < (60)
0x08, 0x14, 0x22, 0x41, 0x00,
// = (61)
0x14, 0x14, 0x14, 0x14, 0x14,
// > (62)
0x00, 0x41, 0x22, 0x14, 0x08,
// ? (63)
0x02, 0x01, 0x51, 0x09, 0x06,
// @ (64)
0x32, 0x49, 0x79, 0x41, 0x3e,
// A (65)
0x7e, 0x11, 0x11, 0x11, 0x7e,
// B (66)
0x7f, 0x49, 0x49, 0x49, 0x36,
// C (67)
0x3e, 0x41, 0x41, 0x41, 0x22,
// D (68)
0x7f, 0x41, 0x41, 0x22, 0x1c,
// E (69)
0x7f, 0x49, 0x49, 0x49, 0x41,
// F (70)
0x7f, 0x09, 0x09, 0x09, 0x01,
// G (71)
0x3e, 0x41, 0x49, 0x49, 0x7a,
// H (72)
0x7f, 0x08, 0x08, 0x08, 0x7f,
// I (73)
0x00, 0x41, 0x7f, 0x41, 0x00,
// J (74)
0x20, 0x40, 0x41, 0x3f, 0x01,
// K (75)
0x7f, 0x08, 0x14, 0x22, 0x41,
// L (76)
0x7f, 0x40, 0x40, 0x40, 0x40,
// M (77)
0x7f, 0x02, 0x0c, 0x02, 0x7f,
// N (78)
0x7f, 0x04, 0x08, 0x10, 0x7f,
// O (79)
0x3e, 0x41, 0x41, 0x41, 0x3e,
// P (80)
0x7f, 0x09, 0x09, 0x09, 0x06,
// Q (81)
0x3e, 0x41, 0x51, 0x21, 0x5e,
// R (82)
0x7f, 0x09, 0x19, 0x29, 0x46,
// S (83)
0x46, 0x49, 0x49, 0x49, 0x31,
// T (84)
0x01, 0x01, 0x7f, 0x01, 0x01,
// U (85)
0x3f, 0x40, 0x40, 0x40, 0x3f,
// V (86)
0x1f, 0x20, 0x40, 0x20, 0x1f,
// W (87)
0x3f, 0x40, 0x38, 0x40, 0x3f,
// X (88)
0x63, 0x14, 0x08, 0x14, 0x63,
// Y (89)
0x07, 0x08, 0x70, 0x08, 0x07,
// Z (90)
0x61, 0x51, 0x49, 0x45, 0x43,
// [ (91)
0x00, 0x7f, 0x41, 0x41, 0x00,
// \ (92)
0x02, 0x04, 0x08, 0x10, 0x20,
// ] (93)
0x00, 0x41, 0x41, 0x7f, 0x00,
// ^ (94)
0x04, 0x02, 0x01, 0x02, 0x04,
// _ (95)
0x40, 0x40, 0x40, 0x40, 0x40,
// ` (96)
0x00, 0x01, 0x02, 0x04, 0x00,
// a (97)
0x20, 0x54, 0x54, 0x54, 0x78,
// b (98)
0x7f, 0x48, 0x44, 0x44, 0x38,
// c (99)
0x38, 0x44, 0x44, 0x44, 0x20,
// d (100)
0x38, 0x44, 0x44, 0x48, 0x7f,
// e (101)
0x38, 0x54, 0x54, 0x54, 0x18,
// f (102)
0x08, 0x7e, 0x09, 0x01, 0x02,
// g (103)
0x0c, 0x52, 0x52, 0x52, 0x3e,
// h (104)
0x7f, 0x08, 0x04, 0x04, 0x78,
// i (105)
0x00, 0x44, 0x7d, 0x40, 0x00,
// j (106)
0x20, 0x40, 0x44, 0x3d, 0x00,
// k (107)
0x7f, 0x10, 0x28, 0x44, 0x00,
// l (108)
0x00, 0x41, 0x7f, 0x40, 0x00,
// m (109)
0x7c, 0x04, 0x18, 0x04, 0x78,
// n (110)
0x7c, 0x08, 0x04, 0x04, 0x78,
// o (111)
0x38, 0x44, 0x44, 0x44, 0x38,
// p (112)
0x7c, 0x14, 0x14, 0x14, 0x08,
// q (113)
0x08, 0x14, 0x14, 0x18, 0x7c,
// r (114)
0x7c, 0x08, 0x04, 0x04, 0x08,
// s (115)
0x48, 0x54, 0x54, 0x54, 0x20,
// t (116)
0x04, 0x3f, 0x44, 0x40, 0x20,
// u (117)
0x3c, 0x40, 0x40, 0x20, 0x7c,
// v (118)
0x1c, 0x20, 0x40, 0x20, 0x1c,
// w (119)
0x3c, 0x40, 0x30, 0x40, 0x3c,
// x (120)
0x44, 0x28, 0x10, 0x28, 0x44,
// y (121)
0x0c, 0x50, 0x50, 0x50, 0x3c,
// z (122)
0x44, 0x64, 0x54, 0x4c, 0x44,
// { (123)
0x00, 0x08, 0x36, 0x41, 0x00,
// | (124)
0x00, 0x00, 0x7f, 0x00, 0x00,
// } (125)
0x00, 0x41, 0x36, 0x08, 0x00,
// ~ (126)
0x10, 0x08, 0x08, 0x10, 0x08,
]),
};
// Alias for compatibility
export const SimpleFont = Font_6x8;
+50
View File
@@ -0,0 +1,50 @@
/**
* Meshtastic OLED Emulator
*
* A Vue.js component and TypeScript library for emulating SSD1306/SH1106 OLED displays.
* Provides the same OLEDDisplay API as the Meshtastic firmware, enabling
* screen development and testing in the browser.
*/
// Core display class
export {
OLEDDisplay,
type OLEDDISPLAY_GEOMETRY,
type OLEDDISPLAY_COLOR,
type OLEDDISPLAY_TEXT_ALIGNMENT,
type OLEDFont,
} from "./OLEDDisplay";
export {
WHITE,
BLACK,
INVERSE,
TEXT_ALIGN_LEFT,
TEXT_ALIGN_CENTER,
TEXT_ALIGN_RIGHT,
} from "./OLEDDisplay";
// Fonts
export {
ArialMT_Plain_10,
ArialMT_Plain_16,
FONT_HEIGHT_SMALL,
FONT_HEIGHT_MEDIUM,
FONT_HEIGHT_LARGE,
createFontFromBytes,
} from "./OLEDFonts";
// Simple working fonts (recommended)
export { Font_6x8, Font_5x7, SimpleFont } from "./SimpleFonts";
// Images and icons
export * as OLEDImages from "./OLEDImages";
// Screen renderers (firmware UI ports)
export * as ScreenRenderers from "./ScreenRenderers";
// Vue component
export { default as OLEDEmulator } from "./OLEDEmulator.vue";
export { DISPLAY_PRESETS, type DisplayPreset } from "./DisplayPresets";
// Demo component
export { default as OLEDEmulatorDemo } from "./Demo.vue";
+4
View File
@@ -0,0 +1,4 @@
import { createApp } from "vue";
import Demo from "./Demo.vue";
createApp(Demo).mount("#app");
+28
View File
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Vue */
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["node_modules", "dist"]
}
+35
View File
@@ -0,0 +1,35 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import dts from "vite-plugin-dts";
import { resolve } from "path";
export default defineConfig({
plugins: [
vue(),
dts({
insertTypesEntry: true,
}),
],
build: {
lib: {
entry: resolve(__dirname, "src/index.ts"),
name: "MeshtasticOLEDEmulator",
formats: ["es", "cjs"],
fileName: (format) => `index.${format === "es" ? "mjs" : "js"}`,
},
rollupOptions: {
external: ["vue"],
output: {
globals: {
vue: "Vue",
},
},
},
cssCodeSplit: false,
},
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
});
+56 -44
View File
@@ -77,10 +77,10 @@ lib_deps =
https://github.com/meshtastic/TinyGPSPlus/archive/71a82db35f3b973440044c476d4bcdc673b104f4.zip
# renovate: datasource=git-refs depName=meshtastic-ArduinoThread packageName=https://github.com/meshtastic/ArduinoThread gitBranch=master
https://github.com/meshtastic/ArduinoThread/archive/b841b0415721f1341ea41cccfb4adccfaf951567.zip
# renovate: datasource=github-tags depName=Nanopb packageName=nanopb/nanopb
https://github.com/nanopb/nanopb/archive/refs/tags/nanopb-0.4.9.1.zip
# renovate: datasource=github-tags depName=ErriezCRC32 packageName=Erriez/ErriezCRC32
https://github.com/Erriez/ErriezCRC32/archive/refs/tags/1.0.1.zip
# renovate: datasource=custom.pio depName=Nanopb packageName=nanopb/library/Nanopb
nanopb/Nanopb@0.4.91
# renovate: datasource=custom.pio depName=ErriezCRC32 packageName=erriez/library/ErriezCRC32
erriez/ErriezCRC32@1.0.1
; Used for the code analysis in PIO Home / Inspect
check_tool = cppcheck
@@ -95,38 +95,34 @@ check_flags =
framework = arduino
lib_deps =
${env.lib_deps}
# renovate: datasource=github-tags depName=NonBlockingRTTTL packageName=end2endzone/NonBlockingRTTTL
https://github.com/end2endzone/NonBlockingRTTTL/archive/refs/tags/1.4.0.zip
build_unflags =
-std=c++11
-std=gnu++11
# renovate: datasource=custom.pio depName=NonBlockingRTTTL packageName=end2endzone/library/NonBlockingRTTTL
end2endzone/NonBlockingRTTTL@1.4.0
build_flags = ${env.build_flags} -Os
-std=gnu++17
build_src_filter = ${env.build_src_filter} -<platform/portduino/> -<graphics/niche/>
; Common libs for communicating over TCP/IP networks such as MQTT
[networking_base]
lib_deps =
# renovate: datasource=github-tags depName=TBPubSubClient packageName=thingsboard/pubsubclient
https://github.com/thingsboard/pubsubclient/archive/refs/tags/v2.12.1.zip
# renovate: datasource=github-tags depName=NTPClient packageName=arduino-libraries/NTPClient
https://github.com/arduino-libraries/NTPClient/archive/refs/tags/3.2.1.zip
# renovate: datasource=custom.pio depName=TBPubSubClient packageName=thingsboard/library/TBPubSubClient
thingsboard/TBPubSubClient@2.12.1
# renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
arduino-libraries/NTPClient@3.2.1
; Extra TCP/IP networking libs for supported devices
[networking_extra]
lib_deps =
# renovate: datasource=github-tags depName=Syslog packageName=arcao/Syslog
https://github.com/arcao/Syslog/archive/refs/tags/v2.0.zip
# renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog
arcao/Syslog@2.0.0
[radiolib_base]
lib_deps =
# renovate: datasource=github-tags depName=RadioLib packageName=jgromes/RadioLib
https://github.com/jgromes/RadioLib/archive/refs/tags/7.6.0.zip
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
jgromes/RadioLib@7.5.0
[device-ui_base]
lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/7b1485b86c8d55a40e5226749097767e8b87f396.zip
https://github.com/meshtastic/device-ui/archive/6c75195e9987b7a49563232234f2f868dd343cae.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]
@@ -142,7 +138,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library
adafruit/Adafruit BME280 Library@2.3.0
# renovate: datasource=custom.pio depName=Adafruit DPS310 packageName=adafruit/library/Adafruit DPS310
adafruit/Adafruit DPS310@1.1.6
adafruit/Adafruit DPS310@1.1.5
# renovate: datasource=custom.pio depName=Adafruit MCP9808 packageName=adafruit/library/Adafruit MCP9808 Library
adafruit/Adafruit MCP9808 Library@2.0.2
# renovate: datasource=custom.pio depName=Adafruit INA260 packageName=adafruit/library/Adafruit INA260 Library
@@ -154,7 +150,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH
adafruit/Adafruit LIS3DH@1.3.0
# renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0
adafruit/Adafruit AHTX0@2.0.6
adafruit/Adafruit AHTX0@2.0.5
# renovate: datasource=custom.pio depName=Adafruit LSM6DS packageName=adafruit/library/Adafruit LSM6DS
adafruit/Adafruit LSM6DS@4.7.4
# renovate: datasource=custom.pio depName=Adafruit TSL2591 packageName=adafruit/library/Adafruit TSL2591 Library
@@ -162,7 +158,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=EmotiBit MLX90632 packageName=emotibit/library/EmotiBit MLX90632
emotibit/EmotiBit MLX90632@1.0.8
# renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library
adafruit/Adafruit MLX90614 Library@2.1.6
adafruit/Adafruit MLX90614 Library@2.1.5
# renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow
https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip
# renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass
@@ -184,12 +180,46 @@ lib_deps =
# renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150
dfrobot/DFRobot_BMM150@1.0.0
# renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561
adafruit/Adafruit TSL2561@1.1.3
adafruit/Adafruit TSL2561@1.1.2
# renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/library/BH1750_WE
wollewald/BH1750_WE@1.1.10
; Common environmental sensor libraries (not included in native / portduino)
[environmental_extra_common]
; (not included in native / portduino)
[environmental_extra]
lib_deps =
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
adafruit/Adafruit BMP3XX Library@2.1.6
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
adafruit/Adafruit MAX1704X@1.0.3
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
adafruit/Adafruit SHTC3 Library@1.0.2
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
adafruit/Adafruit LPS2X@2.0.6
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library
adafruit/Adafruit SHT31 Library@2.2.2
# renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library
adafruit/Adafruit VEML7700 Library@2.1.6
# renovate: datasource=custom.pio depName=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library
adafruit/Adafruit SHT4x Library@1.0.5
# renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
closedcube/ClosedCube OPT3001@1.1.2
# renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2
boschsensortec/bsec2@1.10.2610
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library
boschsensortec/BME68x Sensor Library@1.3.40408
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
sensirion/Sensirion Core@0.7.3
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
sensirion/Sensirion I2C SFA3x@1.0.0
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
[environmental_extra_no_bsec]
lib_deps =
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
adafruit/Adafruit BMP3XX Library@2.1.6
@@ -215,23 +245,5 @@ lib_deps =
sensirion/Sensirion Core@0.7.3
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
sensirion/Sensirion I2C SFA3x@1.0.0
# renovate: datasource=custom.pio depName=Sensirion I2C SCD30 packageName=sensirion/library/Sensirion I2C SCD30
sensirion/Sensirion I2C SCD30@1.0.0
; Environmental sensors with BSEC2 (Bosch proprietary IAQ)
[environmental_extra]
lib_deps =
${environmental_extra_common.lib_deps}
# renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2
boschsensortec/bsec2@1.10.2610
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library
boschsensortec/BME68x Sensor Library@1.3.40408
; Environmental sensors without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
[environmental_extra_no_bsec]
lib_deps =
${environmental_extra_common.lib_deps}
# renovate: datasource=custom.pio depName=Adafruit_BME680 packageName=adafruit/library/Adafruit BME680 Library
adafruit/Adafruit BME680 Library@2.0.6
-2
View File
@@ -4,8 +4,6 @@
":dependencyDashboard",
":semanticCommitTypeAll(chore)",
":ignoreModulesAndTests",
":noUnscheduledUpdates",
"schedule:daily",
"group:recommended",
"replacements:all",
"workarounds:all"
-2
View File
@@ -7,8 +7,6 @@
#include "sleep.h"
#ifdef HAS_NCP5623
#include <Wire.h>
#include <NCP5623.h>
#endif
+16 -11
View File
@@ -4,7 +4,6 @@
#include "configuration.h"
#include "main.h"
#include "sleep.h"
#include <memory>
#ifdef HAS_I2S
#include <AudioFileSourcePROGMEM.h>
@@ -30,9 +29,9 @@ class AudioThread : public concurrency::OSThread
io.digitalWrite(EXPANDS_AMP_EN, HIGH);
#endif
setCPUFast(true);
rtttlFile = std::unique_ptr<AudioFileSourcePROGMEM>(new AudioFileSourcePROGMEM(data, len));
i2sRtttl = std::unique_ptr<AudioGeneratorRTTTL>(new AudioGeneratorRTTTL());
i2sRtttl->begin(rtttlFile.get(), audioOut.get());
rtttlFile = new AudioFileSourcePROGMEM(data, len);
i2sRtttl = new AudioGeneratorRTTTL();
i2sRtttl->begin(rtttlFile, audioOut);
}
// Also handles actually playing the RTTTL, needs to be called in loop
@@ -48,10 +47,14 @@ class AudioThread : public concurrency::OSThread
{
if (i2sRtttl != nullptr) {
i2sRtttl->stop();
delete i2sRtttl;
i2sRtttl = nullptr;
}
rtttlFile = nullptr;
if (rtttlFile != nullptr) {
delete rtttlFile;
rtttlFile = nullptr;
}
setCPUFast(false);
#ifdef T_LORA_PAGER
@@ -63,14 +66,16 @@ class AudioThread : public concurrency::OSThread
{
if (i2sRtttl != nullptr) {
i2sRtttl->stop();
delete i2sRtttl;
i2sRtttl = nullptr;
}
#ifdef T_LORA_PAGER
io.digitalWrite(EXPANDS_AMP_EN, HIGH);
#endif
auto sam = std::unique_ptr<ESP8266SAM>(new ESP8266SAM);
sam->Say(audioOut.get(), text);
ESP8266SAM *sam = new ESP8266SAM;
sam->Say(audioOut, text);
delete sam;
setCPUFast(false);
#ifdef T_LORA_PAGER
io.digitalWrite(EXPANDS_AMP_EN, LOW);
@@ -91,15 +96,15 @@ class AudioThread : public concurrency::OSThread
private:
void initOutput()
{
audioOut = std::unique_ptr<AudioOutputI2S>(new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S));
audioOut = new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S);
audioOut->SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_MCLK);
audioOut->SetGain(0.2);
};
std::unique_ptr<AudioGeneratorRTTTL> i2sRtttl = nullptr;
std::unique_ptr<AudioOutputI2S> audioOut = nullptr;
AudioGeneratorRTTTL *i2sRtttl = nullptr;
AudioOutputI2S *audioOut = nullptr;
std::unique_ptr<AudioFileSourcePROGMEM> rtttlFile = nullptr;
AudioFileSourcePROGMEM *rtttlFile = nullptr;
};
#endif
+2 -10
View File
@@ -98,6 +98,7 @@ Syslog &Syslog::logMask(uint8_t priMask)
void Syslog::enable()
{
this->_client->begin(this->_port);
this->_enabled = true;
}
@@ -165,21 +166,14 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess
if ((pri & LOG_FACMASK) == 0)
pri = LOG_MAKEPRI(LOG_FAC(this->_priDefault), pri);
// W5100S: acquire UDP socket on-demand to avoid permanent socket consumption
if (!this->_client->begin(this->_port)) {
return false;
}
if (this->_server != NULL) {
result = this->_client->beginPacket(this->_server, this->_port);
} else {
result = this->_client->beginPacket(this->_ip, this->_port);
}
if (result != 1) {
this->_client->stop();
if (result != 1)
return false;
}
this->_client->print('<');
this->_client->print(pri);
@@ -199,8 +193,6 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess
this->_client->print(message);
this->_client->endPacket();
this->_client->stop(); // W5100S: release UDP socket for other services
return true;
}
+1 -2
View File
@@ -4,8 +4,7 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
bool usePreset)
{
// If use_preset is false, always return "Custom" — callers such as RadioInterface and Channels
// rely on this being a stable literal for channel-name hashing and default-channel detection.
// If use_preset is false, always return "Custom"
if (!usePreset) {
return "Custom";
}
+4 -14
View File
@@ -35,11 +35,6 @@
#include "nrfx_power.h"
#endif
#if defined(ARCH_NRF52)
#include "Nrf52SaadcLock.h"
#include "concurrency/LockGuard.h"
#endif
#if defined(DEBUG_HEAP_MQTT) && !MESHTASTIC_EXCLUDE_MQTT
#include "mqtt/MQTT.h"
#include "target_specific.h"
@@ -333,9 +328,6 @@ class AnalogBatteryLevel : public HasBatteryLevel
scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs);
scaled *= operativeAdcMultiplier;
#else // block for all other platforms
#ifdef ARCH_NRF52
concurrency::LockGuard saadcGuard(concurrency::nrf52SaadcLock);
#endif
for (uint32_t i = 0; i < BATTERY_SENSE_SAMPLES; i++) {
raw += analogRead(BATTERY_PIN);
}
@@ -712,11 +704,11 @@ bool Power::setup()
found = true;
} else if (analogInit()) {
found = true;
} else {
#ifdef NRF_APM
found = true;
#endif
}
#ifdef NRF_APM
found = true;
#endif
#ifdef EXT_PWR_DETECT
attachInterrupt(
EXT_PWR_DETECT,
@@ -854,10 +846,8 @@ void Power::readPowerStatus()
if (batteryLevel) {
hasBattery = batteryLevel->isBatteryConnect() ? OptTrue : OptFalse;
#ifndef NRF_APM
usbPowered = batteryLevel->isVbusIn() ? OptTrue : OptFalse;
isChargingNow = batteryLevel->isCharging() ? OptTrue : OptFalse;
#endif
if (hasBattery) {
batteryVoltageMv = batteryLevel->getBattVoltage();
// If the AXP192 returns a valid battery percentage, use it

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