diff --git a/.github/actions/build-variant/action.yml b/.github/actions/build-variant/action.yml index e937966149e..8dab42f0ec6 100644 --- a/.github/actions/build-variant/action.yml +++ b/.github/actions/build-variant/action.yml @@ -76,7 +76,7 @@ runs: done - name: PlatformIO ${{ inputs.arch }} download cache - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ~/.platformio/.cache key: pio-cache-${{ inputs.arch }}-${{ hashFiles('.github/actions/**', '**.ini') }} diff --git a/.github/actions/setup-base/action.yml b/.github/actions/setup-base/action.yml index 8e461998a3a..74e0e0e8d93 100644 --- a/.github/actions/setup-base/action.yml +++ b/.github/actions/setup-base/action.yml @@ -5,7 +5,7 @@ runs: using: composite steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: recursive diff --git a/.github/workflows/build_debian_src.yml b/.github/workflows/build_debian_src.yml index 8d2076b113f..066727cff72 100644 --- a/.github/workflows/build_debian_src.yml +++ b/.github/workflows/build_debian_src.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: recursive path: meshtasticd diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml index 47010468849..bafa63d3ea9 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -23,7 +23,7 @@ jobs: outputs: artifact-id: ${{ steps.upload-firmware.outputs.artifact-id }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: submodules: recursive diff --git a/.github/workflows/build_macos_bin.yml b/.github/workflows/build_macos_bin.yml index d0e89d7da6e..ccd64931885 100644 --- a/.github/workflows/build_macos_bin.yml +++ b/.github/workflows/build_macos_bin.yml @@ -16,7 +16,7 @@ jobs: runs-on: macos-${{ inputs.macos_ver }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: recursive diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml index 706b9cfe79b..ab16a82dbcf 100644 --- a/.github/workflows/build_one_target.yml +++ b/.github/workflows/build_one_target.yml @@ -43,7 +43,7 @@ jobs: - stm32 runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-python@v6 with: python-version: 3.x @@ -64,7 +64,7 @@ jobs: if: ${{ inputs.target != '' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT @@ -93,7 +93,7 @@ jobs: needs: [version, build] steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 8a3ef0e6cd7..03a1f91cd4c 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -47,7 +47,7 @@ jobs: runs-on: ${{ inputs.runs-on }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: recursive diff --git a/.github/workflows/docker_manifest.yml b/.github/workflows/docker_manifest.yml index 4bfdfe37e47..92c71bd852b 100644 --- a/.github/workflows/docker_manifest.yml +++ b/.github/workflows/docker_manifest.yml @@ -103,7 +103,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: recursive diff --git a/.github/workflows/flasher-link-comment.yml b/.github/workflows/flasher-link-comment.yml new file mode 100644 index 00000000000..8fdbdf2853a --- /dev/null +++ b/.github/workflows/flasher-link-comment.yml @@ -0,0 +1,190 @@ +name: Post Web Flasher Link Comment + +on: + workflow_run: + workflows: [CI] + types: [completed] + +permissions: + pull-requests: write + actions: read + +jobs: + post-flasher-link: + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion != 'cancelled' && + github.repository == 'meshtastic/firmware' + continue-on-error: true + runs-on: ubuntu-latest + steps: + # Per-board manifests carry the firmware's own metadata (activelySupported, + # displayName, ...) generated from each target's custom_meshtastic_* config. + - name: Download board manifests + uses: actions/download-artifact@v8 + continue-on-error: true + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + pattern: manifest-* + path: ./manifests + merge-multiple: true + + - name: Post or update web flasher link comment + uses: actions/github-script@v9 + with: + script: | + const marker = ''; + const run = context.payload.workflow_run; + const { owner, repo } = context.repo; + + // Resolve the PR number (run.pull_requests is empty for fork PRs) + let prNumber = run.pull_requests?.[0]?.number; + if (!prNumber) { + const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner, repo, commit_sha: run.head_sha, + }); + prNumber = (prs.find((pr) => pr.head.sha === run.head_sha) ?? prs[0])?.number; + } + if (!prNumber) { + core.info('No pull request associated with this run; skipping.'); + return; + } + + const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber }); + + // Only comment on PRs authored by members of the organization. + // author_association MEMBER is computed by GitHub and reflects org + // membership (including concealed members); OWNER covers a repo owner. + const allowedAssociations = ['OWNER', 'MEMBER']; + if (!allowedAssociations.includes(pr.author_association)) { + core.info(`Author association ${pr.author_association} is not an org member; skipping.`); + return; + } + if (pr.state !== 'open') { + core.info('Pull request is not open; skipping.'); + return; + } + if (pr.head.sha !== run.head_sha) { + core.info('Run is for an outdated commit; skipping.'); + return; + } + + // Require at least one per-arch firmware artifact from gather-artifacts + const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { + owner, repo, run_id: run.id, per_page: 100, + }); + const archRe = /^firmware-(esp32|esp32s3|esp32c3|esp32c6|nrf52840|rp2040|rp2350|stm32)-(\d+\.\d+\.\d+\.[0-9a-f]+)$/; + const archArtifacts = artifacts.filter((a) => archRe.test(a.name) && !a.expired); + if (archArtifacts.length === 0) { + core.info('No per-arch firmware artifacts found; skipping.'); + return; + } + + const version = archRe.exec(archArtifacts[0].name)[2]; + const expiresAt = archArtifacts[0].expires_at + ? new Date(archArtifacts[0].expires_at).toISOString().slice(0, 10) + : null; + + // Read each built board's manifest (.mt.json). activelySupported, + // displayName and architecture come straight from the board's + // custom_meshtastic_* platformio config, so the list is in sync with + // the firmware itself — no external device database needed. + const fs = require('fs'); + let boards = []; + try { + boards = fs.readdirSync('./manifests') + .filter((f) => f.endsWith('.mt.json')) + .map((f) => { + try { return JSON.parse(fs.readFileSync(`./manifests/${f}`, 'utf8')); } + catch { return null; } + }) + .filter((m) => m && m.activelySupported === true && m.platformioTarget) + .map((m) => ({ + board: m.platformioTarget, + platform: m.architecture || '', + // displayName is maintainer-authored text; escape table-breaking pipes + displayName: String(m.displayName || m.platformioTarget).replace(/\|/g, '\\|'), + image: Array.isArray(m.images) && m.images[0] ? String(m.images[0]) : '', + })) + .sort((a, b) => a.board.localeCompare(b.board)); + } catch (e) { + core.warning(`Could not read board manifests: ${e.message}`); + } + + const flasherUrl = `https://flasher.meshtastic.org/?pr=${prNumber}`; + // Device illustrations are served by the flasher from the same image + // names the manifest declares (custom_meshtastic_images). The flasher + // serves its SPA shell (HTML, 200) for unknown paths, so confirm each + // image really resolves to an image before linking it. + const imageBase = 'https://flasher.meshtastic.org/img/devices/'; + await Promise.all(boards.map(async (b) => { + if (!b.image) return; + try { + const res = await fetch(`${imageBase}${encodeURIComponent(b.image)}`); + const type = res.headers.get('content-type') || ''; + if (!res.ok || !type.startsWith('image/')) b.image = ''; + } catch { b.image = ''; } + })); + + const boardLines = boards + .map((b) => { + const img = b.image ? `` : ''; + return `| ${img} | ${b.displayName} | [\`${b.board}\`](${flasherUrl}&device=${encodeURIComponent(b.board)}) | ${b.platform} |`; + }) + .join('\n'); + + // Shields.io badges. Only non-user-controlled, charset-constrained values + // (version, commit sha, counts, dates) go into badge URLs — never board + // names or the PR title — so the rendered comment cannot be spoofed. + const shieldText = (s) => + encodeURIComponent(String(s).replace(/-/g, '--').replace(/_/g, '__').replace(/ /g, '_')); + const shield = (label, message, color) => + `https://img.shields.io/badge/${shieldText(label)}-${shieldText(message)}-${color}`; + const buttonUrl = + `https://img.shields.io/badge/${shieldText('Flash this PR in the Web Flasher')}-2C2D3C?style=for-the-badge`; + const badges = [ + `![firmware](${shield('firmware', version, '67EA94')})`, + `![commit](${shield('commit', run.head_sha.slice(0, 7), '2C2D3C')})`, + `![boards](${shield('boards', boards.length, '5C6BC0')})`, + ]; + if (expiresAt) badges.push(`![expires](${shield('expires', expiresAt, '9A4E00')})`); + + // Only render the board table when there are supported boards to list + const boardTable = boards.length > 0 ? [ + `
Supported boards built by this PR (${boards.length})`, + '', + '| | Device | Board | Platform |', + '| --- | --- | --- | --- |', + boardLines, + '', + '
', + '', + ] : []; + + const body = [ + marker, + '## ⚡ Try this PR in the Web Flasher', + '', + `[![Flash this PR in the Web Flasher](${buttonUrl})](${flasherUrl})`, + '', + badges.join(' '), + '', + '> [!WARNING]', + '> This is an automated, unreviewed CI test build. Back up your device configuration', + '> before flashing, and only flash devices you are able to recover.', + '', + ...boardTable, + `*Build artifacts expire${expiresAt ? ` on ${expiresAt}` : ' after 30 days'}. Updated for \`${run.head_sha.slice(0, 7)}\`.*`, + ].join('\n'); + + // Sticky comment: update in place when the marker is found + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number: prNumber, per_page: 100, + }); + const existing = comments.find((c) => c.body?.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body }); + } diff --git a/.github/workflows/flasher-link-placeholder.yml b/.github/workflows/flasher-link-placeholder.yml new file mode 100644 index 00000000000..0f6e8f49090 --- /dev/null +++ b/.github/workflows/flasher-link-placeholder.yml @@ -0,0 +1,60 @@ +name: Post Web Flasher Build Placeholder + +# Drops an immediate "build in progress" comment when a PR opens, so the web +# flasher entry shows up right away. The real CI-driven workflow +# (flasher-link-comment.yml) later replaces it in place via the shared marker. +# +# SECURITY: this uses pull_request_target (write token, runs for fork PRs) but is +# safe because it never checks out or runs PR code and posts a fully static body +# — no PR title, branch name, or other untrusted input is used anywhere. + +on: + pull_request_target: + types: [opened, reopened] + +permissions: + pull-requests: write + +jobs: + post-placeholder: + if: github.repository == 'meshtastic/firmware' + continue-on-error: true + runs-on: ubuntu-latest + steps: + - name: Post web flasher build-in-progress placeholder + uses: actions/github-script@v9 + with: + script: | + const marker = ''; + const { owner, repo } = context.repo; + const pr = context.payload.pull_request; + + // Only org members get the flasher comment (matches the real workflow) + const allowedAssociations = ['OWNER', 'MEMBER']; + if (!allowedAssociations.includes(pr.author_association)) { + core.info(`Author association ${pr.author_association} is not an org member; skipping.`); + return; + } + + // Only seed a placeholder when no flasher comment exists yet — never + // overwrite a real (or existing placeholder) comment. + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number: pr.number, per_page: 100, + }); + if (comments.some((c) => c.body?.includes(marker))) { + core.info('Flasher comment already exists; nothing to do.'); + return; + } + + const body = [ + marker, + '## ⚡ Try this PR in the Web Flasher', + '', + '> [!NOTE]', + '> Building this pull request… the flash button, badges and supported-board', + '> list will appear here automatically once CI finishes.', + ].join('\n'); + + await github.rest.issues.createComment({ + owner, repo, issue_number: pr.number, body, + }); diff --git a/.github/workflows/hook_copr.yml b/.github/workflows/hook_copr.yml index c419848a863..aba7e9c0dd0 100644 --- a/.github/workflows/hook_copr.yml +++ b/.github/workflows/hook_copr.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: recursive diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 3505d950e35..5a681aa46df 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -40,7 +40,7 @@ jobs: - check runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-python@v6 with: python-version: 3.x @@ -64,7 +64,7 @@ jobs: version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT @@ -86,7 +86,7 @@ jobs: runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }} if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: submodules: recursive - name: Check ${{ matrix.check.board }} @@ -189,7 +189,7 @@ jobs: needs: [version, build] steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - uses: actions/download-artifact@v8 with: @@ -245,48 +245,6 @@ jobs: path: ./*.elf retention-days: 30 - shame: - if: github.repository == 'meshtastic/firmware' - continue-on-error: true - runs-on: ubuntu-latest - needs: [build] - steps: - - uses: actions/checkout@v6 - if: github.event_name == 'pull_request' - 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 - 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 - 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' - 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' - # 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' - # run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/ - release-artifacts: permissions: # Needed for 'gh release upload'. contents: write @@ -303,7 +261,7 @@ jobs: # - MacOS steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -333,6 +291,7 @@ jobs: prerelease: true name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha tag_name: v${{ needs.version.outputs.long }} + target_commitish: ${{ github.sha }} body: ${{ steps.release_notes.outputs.notes }} - name: Download source deb @@ -403,7 +362,7 @@ jobs: needs: [release-artifacts, version] steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup Python uses: actions/setup-python@v6 @@ -458,7 +417,7 @@ jobs: esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 diff --git a/.github/workflows/models_issue_triage.yml b/.github/workflows/models_issue_triage.yml deleted file mode 100644 index a02646ea09b..00000000000 --- a/.github/workflows/models_issue_triage.yml +++ /dev/null @@ -1,213 +0,0 @@ -name: Issue Triage (Models) - -on: - issues: - types: [opened] - -permissions: - issues: write - models: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.issue.number }} - cancel-in-progress: true - -jobs: - triage: - if: ${{ github.repository == 'meshtastic/firmware' && github.event.issue.user.type != 'Bot' }} - runs-on: ubuntu-latest - steps: - # ───────────────────────────────────────────────────────────────────────── - # Step 1: Quality check (spam/AI-slop detection) - runs first, exits early if spam - # ───────────────────────────────────────────────────────────────────────── - - name: Detect spam or low-quality content - uses: actions/ai-inference@v2 - id: quality - continue-on-error: true - with: - max-tokens: 20 - prompt: | - Is this GitHub issue spam, AI-generated slop, or low quality? - - Title: ${{ github.event.issue.title }} - Body: ${{ github.event.issue.body }} - - Respond with exactly one of: spam, ai-generated, needs-review, ok - system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop. - model: openai/gpt-4o-mini - - - name: Apply quality label if needed - if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok' - uses: actions/github-script@v9 - env: - QUALITY_LABEL: ${{ steps.quality.outputs.response }} - with: - script: | - const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase(); - const labelMeta = { - 'spam': { color: 'd73a4a', description: 'Possible spam' }, - 'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' }, - 'needs-review': { color: 'f9d0c4', description: 'Needs human review' }, - }; - const meta = labelMeta[label]; - if (!meta) return; - - // Ensure label exists - try { - await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); - } catch (e) { - if (e.status !== 404) throw e; - await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); - } - - // Apply label - await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] }); - - // Set output to skip remaining steps - core.setOutput('is_spam', 'true'); - - # ───────────────────────────────────────────────────────────────────────── - # Step 2: Duplicate detection - only if not spam - # ───────────────────────────────────────────────────────────────────────── - - name: Detect duplicate issues - if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '' - uses: pelikhan/action-genai-issue-dedup@bdb3b5d9451c1090ffcdf123d7447a5e7c7a2528 # v0.0.19 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - - # ───────────────────────────────────────────────────────────────────────── - # Step 3: Completeness check + auto-labeling (combined into one AI call) - # ───────────────────────────────────────────────────────────────────────── - - name: Determine if completeness check should be skipped - if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '' - uses: actions/github-script@v9 - id: check-skip - with: - script: | - const title = (context.payload.issue.title || '').toLowerCase(); - const labels = (context.payload.issue.labels || []).map(label => label.name); - const hasFeatureRequest = title.includes('feature request'); - const hasEnhancement = labels.includes('enhancement'); - const shouldSkip = hasFeatureRequest && hasEnhancement; - core.setOutput('should_skip', shouldSkip ? 'true' : 'false'); - - - name: Analyze issue completeness and determine labels - if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' - uses: actions/ai-inference@v2 - id: analysis - continue-on-error: true - with: - prompt: | - Analyze this GitHub issue for completeness and determine if it needs labels. - - IMPORTANT: Distinguish between: - - Device/firmware bugs (crashes, reboots, lockups, radio/GPS/display/power issues) - these need device logs - - Build/release/packaging issues (missing files, CI failures, download problems) - these do NOT need device logs - - Documentation or website issues - these do NOT need device logs - - If this is a device/firmware bug, request device logs and explain how to get them: - - Web Flasher logs: - - Go to https://flasher.meshtastic.org - - Connect the device via USB and click Connect - - Open the device console/log output, reproduce the problem, then copy/download and attach/paste the logs - - Meshtastic CLI logs: - - Run: meshtastic --port --noproto - - Reproduce the problem, then copy/paste the terminal output - - Also request key context if missing: device model/variant, firmware version, region, steps to reproduce, expected vs actual. - - Respond ONLY with valid JSON (no markdown, no code fences): - {"complete": true, "comment": "", "label": "none"} - OR - {"complete": false, "comment": "Your helpful comment", "label": "needs-logs"} - - Use "needs-logs" ONLY if this is a device/firmware bug AND no logs are attached. - Use "needs-info" if basic info like firmware version or steps to reproduce are missing. - Use "none" if the issue is complete, is a feature request, or is a build/CI/packaging issue. - - Title: ${{ github.event.issue.title }} - Body: ${{ github.event.issue.body }} - system-prompt: You are a helpful assistant that triages GitHub issues. Be conservative with labels. Only request device logs for actual device/firmware bugs, not for build/release/CI issues. - model: openai/gpt-4o-mini - - - name: Process analysis result - if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != '' - uses: actions/github-script@v9 - id: process - env: - AI_RESPONSE: ${{ steps.analysis.outputs.response }} - with: - script: | - let raw = (process.env.AI_RESPONSE || '').trim(); - - // Strip markdown code fences if present - raw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim(); - - let complete = true; - let comment = ''; - let label = 'none'; - - try { - const parsed = JSON.parse(raw); - complete = !!parsed.complete; - comment = (parsed.comment ?? '').toString().trim(); - label = (parsed.label ?? 'none').toString().trim().toLowerCase(); - } catch { - // If JSON parse fails, log warning and don't comment (avoid posting raw JSON) - console.log('Failed to parse AI response as JSON:', raw); - complete = true; - comment = ''; - label = 'none'; - } - - // Validate label - const allowedLabels = new Set(['needs-logs', 'needs-info', 'none']); - if (!allowedLabels.has(label)) label = 'none'; - - // Only comment if we have a valid parsed comment (not raw JSON) - const shouldComment = !complete && comment.length > 0 && !comment.startsWith('{'); - core.setOutput('should_comment', shouldComment ? 'true' : 'false'); - core.setOutput('comment_body', comment); - core.setOutput('label', label); - - - name: Apply triage label - if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none' - uses: actions/github-script@v9 - env: - LABEL_NAME: ${{ steps.process.outputs.label }} - with: - script: | - const label = process.env.LABEL_NAME; - const labelMeta = { - 'needs-logs': { color: 'cfd3d7', description: 'Device logs requested for triage' }, - 'needs-info': { color: 'f9d0c4', description: 'More information requested for triage' }, - }; - const meta = labelMeta[label]; - if (!meta) return; - - // Ensure label exists - try { - await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); - } catch (e) { - if (e.status !== 404) throw e; - await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); - } - - // Apply label - await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] }); - - - name: Comment on issue - if: steps.process.outputs.should_comment == 'true' - uses: actions/github-script@v9 - env: - COMMENT_BODY: ${{ steps.process.outputs.comment_body }} - with: - script: | - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.issue.number, - body: process.env.COMMENT_BODY - }); diff --git a/.github/workflows/models_pr_triage.yml b/.github/workflows/models_pr_triage.yml deleted file mode 100644 index f39ee4845fb..00000000000 --- a/.github/workflows/models_pr_triage.yml +++ /dev/null @@ -1,139 +0,0 @@ -name: PR Triage (Models) - -on: - pull_request_target: - types: [opened] - -permissions: - pull-requests: write - issues: write - models: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - triage: - if: ${{ github.repository == 'meshtastic/firmware' && github.event.pull_request.user.type != 'Bot' }} - runs-on: ubuntu-latest - steps: - # ───────────────────────────────────────────────────────────────────────── - # Step 1: Check if PR already has automation/type labels (skip if so) - # ───────────────────────────────────────────────────────────────────────── - - name: Check existing labels - uses: actions/github-script@v9 - id: check-labels - with: - script: | - const skipLabels = new Set(['automation']); - const typeLabels = new Set(['bugfix', 'hardware-support', 'enhancement', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']); - const prLabels = context.payload.pull_request.labels.map(l => l.name); - - const shouldSkipAll = prLabels.some(l => skipLabels.has(l)); - const hasTypeLabel = prLabels.some(l => typeLabels.has(l)); - - core.setOutput('skip_all', shouldSkipAll ? 'true' : 'false'); - core.setOutput('has_type_label', hasTypeLabel ? 'true' : 'false'); - - # ───────────────────────────────────────────────────────────────────────── - # Step 2: Quality check (spam/AI-slop detection) - # ───────────────────────────────────────────────────────────────────────── - - name: Detect spam or low-quality content - if: steps.check-labels.outputs.skip_all != 'true' - uses: actions/ai-inference@v2 - id: quality - continue-on-error: true - with: - max-tokens: 20 - prompt: | - Is this GitHub pull request spam, AI-generated slop, or low quality? - - Title: ${{ github.event.pull_request.title }} - Body: ${{ github.event.pull_request.body }} - - Respond with exactly one of: spam, ai-generated, needs-review, ok - system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop. - model: openai/gpt-4o-mini - - - name: Apply quality label if needed - if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok' - uses: actions/github-script@v9 - id: quality-label - env: - QUALITY_LABEL: ${{ steps.quality.outputs.response }} - with: - script: | - const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase(); - const labelMeta = { - 'spam': { color: 'd73a4a', description: 'Possible spam' }, - 'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' }, - 'needs-review': { color: 'f9d0c4', description: 'Needs human review' }, - }; - const meta = labelMeta[label]; - if (!meta) return; - - // Ensure label exists - try { - await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); - } catch (e) { - if (e.status !== 404) throw e; - await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); - } - - // Apply label - await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] }); - - core.setOutput('is_spam', 'true'); - - # ───────────────────────────────────────────────────────────────────────── - # Step 3: Auto-label PR type (bugfix/hardware-support/enhancement) - # Only skip for spam/ai-generated; still classify needs-review PRs - # ───────────────────────────────────────────────────────────────────────── - - name: Classify PR for labeling - if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.quality.outputs.response != 'spam' && steps.quality.outputs.response != 'ai-generated' - uses: actions/ai-inference@v2 - id: classify - continue-on-error: true - with: - max-tokens: 30 - prompt: | - Classify this pull request into exactly one category. - - Return exactly one of: bugfix, hardware-support, enhancement - - Use bugfix if it fixes a bug, crash, or incorrect behavior. - Use hardware-support if it adds or improves support for a specific hardware device/variant. - Use enhancement if it adds a new feature, improves performance, or refactors code. - - Title: ${{ github.event.pull_request.title }} - Body: ${{ github.event.pull_request.body }} - system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label. - model: openai/gpt-4o-mini - - - name: Apply type label - if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != '' - uses: actions/github-script@v9 - env: - TYPE_LABEL: ${{ steps.classify.outputs.response }} - with: - script: | - const label = (process.env.TYPE_LABEL || '').trim().toLowerCase(); - const labelMeta = { - 'bugfix': { color: 'd73a4a', description: 'Bug fix' }, - 'hardware-support': { color: '0e8a16', description: 'Hardware support addition or improvement' }, - 'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' }, - }; - const meta = labelMeta[label]; - if (!meta) return; - - // Ensure label exists - try { - await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); - } catch (e) { - if (e.status !== 404) throw e; - await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); - } - - // Apply label - await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] }); diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 045e94895c7..39931cbfa48 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -14,16 +14,16 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Trunk Check - uses: trunk-io/trunk-action@v1 + uses: trunk-io/trunk-action@v1.3.1 with: trunk-token: ${{ secrets.TRUNK_TOKEN }} trunk_upgrade: if: github.repository == 'meshtastic/firmware' - # See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades + # See: https://github.com/trunk-io/trunk-action/blob/main/readme.md#automatic-upgrades name: Trunk Upgrade (PR) runs-on: ubuntu-24.04 permissions: @@ -31,9 +31,9 @@ jobs: pull-requests: write # For trunk to create PRs steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Trunk Upgrade - uses: trunk-io/trunk-action/upgrade@v1 + uses: trunk-io/trunk-action/upgrade@v1.3.1 with: base: master diff --git a/.github/workflows/package_obs.yml b/.github/workflows/package_obs.yml index b491f006251..520c0851ecf 100644 --- a/.github/workflows/package_obs.yml +++ b/.github/workflows/package_obs.yml @@ -33,7 +33,7 @@ jobs: needs: build-debian-src steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: recursive path: meshtasticd diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml index 6bd256f52cc..bf2576a53e4 100644 --- a/.github/workflows/package_pio_deps.yml +++ b/.github/workflows/package_pio_deps.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: recursive diff --git a/.github/workflows/package_ppa.yml b/.github/workflows/package_ppa.yml index c51e64e783e..88d98e628af 100644 --- a/.github/workflows/package_ppa.yml +++ b/.github/workflows/package_ppa.yml @@ -34,7 +34,7 @@ jobs: needs: build-debian-src steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: recursive path: meshtasticd diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml index e3321712e8a..7d910154cf9 100644 --- a/.github/workflows/pr_tests.yml +++ b/.github/workflows/pr_tests.yml @@ -40,7 +40,7 @@ jobs: checks: write pull-requests: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: submodules: recursive diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index a85979c725a..3184413ef37 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -91,7 +91,7 @@ jobs: shell: bash steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: # Always use master branch for version bumps ref: master diff --git a/.github/workflows/sec_sast_semgrep_cron.yml b/.github/workflows/sec_sast_semgrep_cron.yml index 95e5c2c3d86..da5d60a893a 100644 --- a/.github/workflows/sec_sast_semgrep_cron.yml +++ b/.github/workflows/sec_sast_semgrep_cron.yml @@ -21,7 +21,7 @@ jobs: steps: # step 1 - name: clone application source code - uses: actions/checkout@v6 + uses: actions/checkout@v7 # step 2 - name: full scan diff --git a/.github/workflows/sec_sast_semgrep_pull.yml b/.github/workflows/sec_sast_semgrep_pull.yml index e9b4108a19d..1508e082256 100644 --- a/.github/workflows/sec_sast_semgrep_pull.yml +++ b/.github/workflows/sec_sast_semgrep_pull.yml @@ -13,7 +13,7 @@ jobs: steps: # step 1 - name: clone application source code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index 9255975a8fc..f81c6ef9a85 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Stale PR+Issues - uses: actions/stale@v10.2.0 + uses: actions/stale@v10.3.0 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. diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml index 2fabf0591ed..aaec90979fc 100644 --- a/.github/workflows/test_native.yml +++ b/.github/workflows/test_native.yml @@ -14,7 +14,7 @@ jobs: name: Native Simulator Tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: submodules: recursive @@ -68,7 +68,7 @@ jobs: name: Native PlatformIO Tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: submodules: recursive @@ -123,7 +123,7 @@ jobs: - platformio-tests if: always() steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Get release version string run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index be51428437b..c788b794253 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: runs-on: test-runner steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 # - uses: actions/setup-python@v6 # with: diff --git a/.github/workflows/trunk_annotate_pr.yml b/.github/workflows/trunk_annotate_pr.yml index 59ab25c2810..7baf857dec3 100644 --- a/.github/workflows/trunk_annotate_pr.yml +++ b/.github/workflows/trunk_annotate_pr.yml @@ -1,5 +1,5 @@ name: Annotate PR with trunk issues -# See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#getting-inline-annotations-for-fork-prs +# See: https://github.com/trunk-io/trunk-action/blob/main/readme.md#getting-inline-annotations-for-fork-prs on: workflow_run: @@ -18,9 +18,9 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Trunk Check - uses: trunk-io/trunk-action@v1 + uses: trunk-io/trunk-action@v1.3.1 with: post-annotations: true diff --git a/.github/workflows/trunk_check.yml b/.github/workflows/trunk_check.yml index 874374fe0dc..9d8c785faaf 100644 --- a/.github/workflows/trunk_check.yml +++ b/.github/workflows/trunk_check.yml @@ -16,9 +16,9 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Trunk Check - uses: trunk-io/trunk-action@v1 + uses: trunk-io/trunk-action@v1.3.1 with: save-annotations: true diff --git a/.github/workflows/update_protobufs.yml b/.github/workflows/update_protobufs.yml index e9380467eac..3239461e076 100644 --- a/.github/workflows/update_protobufs.yml +++ b/.github/workflows/update_protobufs.yml @@ -11,18 +11,23 @@ jobs: pull-requests: write steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: true - name: Update submodule - if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' }} + if: ${{ github.ref_name == 'master' || github.ref_name == 'develop' }} + working-directory: protobufs + env: + # Use the branch that triggered the workflow as the protobuf branch. + GIT_BRANCH: ${{ github.ref_name }} run: | - git submodule update --remote protobufs + git fetch --prune origin $GIT_BRANCH + git checkout origin/$GIT_BRANCH - name: Download nanopb run: | - wget https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.9.1-linux-x86.tar.gz + wget https://github.com/nanopb/nanopb/releases/download/nanopb-0.4.9.1/nanopb-0.4.9.1-linux-x86.tar.gz tar xvzf nanopb-0.4.9.1-linux-x86.tar.gz mv nanopb-0.4.9.1-linux-x86 nanopb-0.4.9 @@ -33,7 +38,7 @@ jobs: - name: Create pull request uses: peter-evans/create-pull-request@v8 with: - branch: create-pull-request/update-protobufs + branch: create-pull-request/update-protobufs-${{ github.ref_name }} labels: submodules title: Update protobufs and classes commit-message: Update protobufs diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 0bbfbcf08e6..78e66e855cf 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -4,23 +4,23 @@ cli: plugins: sources: - id: trunk - ref: v1.10.0 + ref: v1.10.2 uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.529 - - renovate@43.150.0 - - prettier@3.8.3 - - trufflehog@3.95.3 + - checkov@3.3.8 + - renovate@43.278.4 + - prettier@3.9.6 + - trufflehog@3.95.9 - yamllint@1.38.0 - bandit@1.9.4 - - trivy@0.70.0 + - trivy@0.72.0 - taplo@0.10.0 - - ruff@0.15.13 + - ruff@0.15.22 - isort@8.0.1 - - markdownlint@0.48.0 + - markdownlint@0.49.1 - oxipng@10.1.1 - - svgo@4.0.1 + - svgo@4.0.2 - actionlint@1.7.12 - flake8@7.3.0 - hadolint@2.14.0 @@ -34,6 +34,7 @@ lint: - linters: [ALL] paths: - bin/** + - branding/** runtimes: enabled: - python@3.14.4 diff --git a/alpine.Dockerfile b/alpine.Dockerfile index 6d1b999e299..2f534bd8ce5 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -4,7 +4,7 @@ # trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions # Ensure the Alpine version is updated in both stages of the container! -FROM alpine:3.23 AS builder +FROM alpine:3.24 AS builder ARG PIO_ENV=native # Enable Alpine community repository (for 'py3-grpcio-tools') @@ -35,7 +35,7 @@ RUN bash ./bin/build-native.sh "$PIO_ENV" && \ # ##### PRODUCTION BUILD ############# -FROM alpine:3.23 +FROM alpine:3.24 LABEL org.opencontainers.image.title="Meshtastic" \ org.opencontainers.image.description="Alpine Meshtastic daemon" \ org.opencontainers.image.url="https://meshtastic.org" \ diff --git a/bin/config.d/lora-NebraHat_1W.yaml b/bin/config.d/lora-NebraHat_1W.yaml new file mode 100644 index 00000000000..9f339371392 --- /dev/null +++ b/bin/config.d/lora-NebraHat_1W.yaml @@ -0,0 +1,19 @@ +# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/NebraHat +# Use for 1 watt hat +Meta: + name: NebraHat 1W + support: community + compatible: + - raspberry-pi + +Lora: + Module: sx1262 # Nebra SX1262 Pi Hat - 1W + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true +# CS: 8 # Newer version of MeshtasticD do not need this? If issues uncomment this line + IRQ: 22 + Busy: 4 + Reset: 18 + RXen: 25 +I2C: + I2CDevice: /dev/i2c-1 \ No newline at end of file diff --git a/bin/config.d/lora-NebraHat_2W.yaml b/bin/config.d/lora-NebraHat_2W.yaml new file mode 100644 index 00000000000..4b712dd8a59 --- /dev/null +++ b/bin/config.d/lora-NebraHat_2W.yaml @@ -0,0 +1,20 @@ +# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/NebraHat +# Use for 2 watt hat +Meta: + name: NebraHat 2W + support: community + compatible: + - raspberry-pi + +Lora: + Module: sx1262 # Nebra SX1262 Pi Hat - 2W + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + SX126X_MAX_POWER: 8 +# CS: 8 # Newer version of MeshtasticD do not need this? If issues uncomment this line + IRQ: 22 + Busy: 4 + Reset: 18 + RXen: 25 +I2C: + I2CDevice: /dev/i2c-1 \ No newline at end of file diff --git a/bin/config.d/lora-RAK6421-13300-slot1.yaml b/bin/config.d/lora-RAK6421-13300-slot1.yaml index a88544896d7..086dadbc554 100644 --- a/bin/config.d/lora-RAK6421-13300-slot1.yaml +++ b/bin/config.d/lora-RAK6421-13300-slot1.yaml @@ -18,4 +18,4 @@ Lora: DIO3_TCXO_VOLTAGE: true DIO2_AS_RF_SWITCH: true spidev: spidev0.0 - # CS: 8 \ No newline at end of file + # CS: 8 diff --git a/bin/config.d/lora-RAK6421-13300-slot2.yaml b/bin/config.d/lora-RAK6421-13300-slot2.yaml index 40b0cea095f..c0ed9017a09 100644 --- a/bin/config.d/lora-RAK6421-13300-slot2.yaml +++ b/bin/config.d/lora-RAK6421-13300-slot2.yaml @@ -5,7 +5,9 @@ Meta: - raspberry-pi Lora: - ### RAK13300 in Slot 2 pins + + ### RAK13300 in Slot 2 + Module: sx1262 IRQ: 18 #IO6 Reset: 24 # IO4 Busy: 19 # IO5 @@ -13,5 +15,7 @@ Lora: Enable_Pins: - 26 - 23 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true spidev: spidev0.1 - # CS: 7 \ No newline at end of file + # CS: 7 diff --git a/bin/config.d/lora-RAK6421-13302-slot2.yaml b/bin/config.d/lora-RAK6421-13302-slot2.yaml index 5aa23911fe1..d4652c16747 100644 --- a/bin/config.d/lora-RAK6421-13302-slot2.yaml +++ b/bin/config.d/lora-RAK6421-13302-slot2.yaml @@ -5,14 +5,18 @@ Meta: - raspberry-pi Lora: - ### RAK13302 in Slot 2 pins + + ### RAK13302 in Slot 2 + Module: sx1262 IRQ: 18 #IO6 Reset: 24 # IO4 Busy: 19 # IO5 - # Ant_sw: 23 # IO3 + # Ant_sw: 23 # IO3 Enable_Pins: - 26 - 23 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true 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] \ No newline at end of file diff --git a/bin/config.d/lora-ZebraHatDuo_R0_1W.yaml b/bin/config.d/lora-ZebraHatDuo_R0_1W.yaml new file mode 100644 index 00000000000..893c74447e3 --- /dev/null +++ b/bin/config.d/lora-ZebraHatDuo_R0_1W.yaml @@ -0,0 +1,23 @@ +# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/NebraHat/Duo +# Use for E22P on Duo Hat +# Use for 1 watt hat - Radio 0 +Meta: + name: ZebraHatDuo Radio 0 1W + support: community + compatible: + - raspberry-pi + +Lora: + Module: sx1262 # + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + SX126X_MAX_POWER: 18 + spidev: spidev0.0 +# CS: 8 # Depending on how overlay is setup + IRQ: 18 + Busy: 23 + Reset: 24 + +# Uncomment below to enable temp/humid sensors +#I2C: +# I2CDevice: /dev/i2c-1 \ No newline at end of file diff --git a/bin/config.d/lora-ZebraHatDuo_R1_1W.yaml b/bin/config.d/lora-ZebraHatDuo_R1_1W.yaml new file mode 100644 index 00000000000..c1e70089d7d --- /dev/null +++ b/bin/config.d/lora-ZebraHatDuo_R1_1W.yaml @@ -0,0 +1,23 @@ +# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/NebraHat/Duo +# Use for E22P on Duo Hat +# Use for 1 watt hat - Radio 1 +Meta: + name: ZebraHatDuo Radio 0 1W + support: community + compatible: + - raspberry-pi + +Lora: + Module: sx1262 # + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + SX126X_MAX_POWER: 18 + spidev: spidev0.1 +# CS: 8 # Depending on how overlay is setup + IRQ: 22 + Busy: 27 + Reset: 17 + +# Uncomment below to enable temp/humid sensors +#I2C: +# I2CDevice: /dev/i2c-1 \ No newline at end of file diff --git a/bin/config.d/lora-ZebraHat_1W.yaml b/bin/config.d/lora-ZebraHat_1W.yaml new file mode 100644 index 00000000000..f0b3c34d3fe --- /dev/null +++ b/bin/config.d/lora-ZebraHat_1W.yaml @@ -0,0 +1,19 @@ +# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/ZebraHAT +# Use for 1 watt hat +Meta: + name: ZebraHat 1W + support: community + compatible: + - raspberry-pi + +Lora: + Module: sx1262 # Zebra SX1262 Pi Hat - 1W + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + SX126X_MAX_POWER: 18 + CS: 24 + IRQ: 22 + Busy: 27 + Reset: 17 +I2C: + I2CDevice: /dev/i2c-1 diff --git a/bin/config.d/lora-ZebraHat_2W.yaml b/bin/config.d/lora-ZebraHat_2W.yaml new file mode 100644 index 00000000000..fe556ea77ad --- /dev/null +++ b/bin/config.d/lora-ZebraHat_2W.yaml @@ -0,0 +1,20 @@ +# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/ZebraHAT +# Use for 2 watt hat +Meta: + name: ZebraHat 2W + support: community + compatible: + - raspberry-pi + +Lora: + Module: sx1262 # Zebra SX1262 Pi Hat - 2W + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + SX126X_MAX_POWER: 8 + CS: 24 + IRQ: 22 + Busy: 27 + Reset: 17 + RXen: 25 +I2C: + I2CDevice: /dev/i2c-1 diff --git a/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot2.yaml b/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot2.yaml index e0ef946d97f..5121e8a61d5 100644 --- a/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot2.yaml +++ b/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot2.yaml @@ -29,6 +29,8 @@ Lora: - pin: 50 # GPIO1_C2 (physical 16) gpiochip: 1 line: 18 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true spidev: spidev0.1 # CS: # GPIO0_A7 (SPI1_CSN1, physical 26) # pin: 7 diff --git a/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot2.yaml b/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot2.yaml index 5548bc5c7b5..0417d36e90e 100644 --- a/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot2.yaml +++ b/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot2.yaml @@ -29,6 +29,8 @@ Lora: - pin: 50 # GPIO1_C2 (physical 16) gpiochip: 1 line: 18 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true spidev: spidev0.1 # CS: # GPIO0_A7 (SPI1_CSN1, physical 26) # pin: 7 diff --git a/bin/config.d/lora-lyra-zero-BQ-SG3-BQ35LORA900V1M-slot1.yaml b/bin/config.d/lora-lyra-zero-BQ-SG3-BQ35LORA900V1M-slot1.yaml new file mode 100644 index 00000000000..0bee549e459 --- /dev/null +++ b/bin/config.d/lora-lyra-zero-BQ-SG3-BQ35LORA900V1M-slot1.yaml @@ -0,0 +1,29 @@ +# Station G3 + BQ35LORA900V1M Primary Slot +# Board Doc: https://wiki.bqvoy.com/en/devkits/station-g3 +Meta: + name: B&Q Station G3 + BQ35LORA900V1M Primary Slot + support: official + compatible: + - luckfox-lyra-zero-w # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO0_A5 (physical 15) + pin: 5 + gpiochip: 0 + line: 5 + Reset: # GPIO1_D1 (physical 36) + pin: 57 + gpiochip: 1 + line: 25 + Busy: # GPIO0_B4 (physical 18) + pin: 12 + gpiochip: 0 + line: 12 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true + spidev: spidev0.0 + # CS: # GPIO0_B2 (physical 24) + # pin: 10 + # gpiochip: 0 + # line: 10 diff --git a/bin/config.d/lora-lyra-zero-RAK6421-13300-slot2.yaml b/bin/config.d/lora-lyra-zero-RAK6421-13300-slot2.yaml index 255a3eca38b..63648432c47 100644 --- a/bin/config.d/lora-lyra-zero-RAK6421-13300-slot2.yaml +++ b/bin/config.d/lora-lyra-zero-RAK6421-13300-slot2.yaml @@ -29,6 +29,8 @@ Lora: - pin: 13 # GPIO0_B5 gpiochip: 0 line: 13 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true spidev: spidev0.1 # CS: # GPIO0_B1 # pin: 9 diff --git a/bin/config.d/lora-lyra-zero-RAK6421-13302-slot2.yaml b/bin/config.d/lora-lyra-zero-RAK6421-13302-slot2.yaml index 773a35ab0fe..d3baebd458c 100644 --- a/bin/config.d/lora-lyra-zero-RAK6421-13302-slot2.yaml +++ b/bin/config.d/lora-lyra-zero-RAK6421-13302-slot2.yaml @@ -29,6 +29,8 @@ Lora: - pin: 13 # GPIO0_B5 gpiochip: 0 line: 13 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true spidev: spidev0.1 # CS: # GPIO0_B1 # pin: 9 diff --git a/bin/config.d/lora-ok3506-RAK6421-13300-slot2.yaml b/bin/config.d/lora-ok3506-RAK6421-13300-slot2.yaml index 969c20ad3f4..225fb5f8dd9 100644 --- a/bin/config.d/lora-ok3506-RAK6421-13300-slot2.yaml +++ b/bin/config.d/lora-ok3506-RAK6421-13300-slot2.yaml @@ -31,6 +31,8 @@ Lora: - pin: 103 # GPIO3_A7 (physical 16) gpiochip: 3 line: 7 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true spidev: spidev0.1 # CS: # GPIO0_B7 (SPI0_CSN1, physical 26) # pin: 15 diff --git a/bin/config.d/lora-ok3506-RAK6421-13302-slot2.yaml b/bin/config.d/lora-ok3506-RAK6421-13302-slot2.yaml index 36b70658b6a..ffb221fba1f 100644 --- a/bin/config.d/lora-ok3506-RAK6421-13302-slot2.yaml +++ b/bin/config.d/lora-ok3506-RAK6421-13302-slot2.yaml @@ -31,6 +31,8 @@ Lora: - pin: 103 # GPIO3_A7 (physical 16) gpiochip: 3 line: 7 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true spidev: spidev0.1 # CS: # GPIO0_B7 (SPI0_CSN1, physical 26) # pin: 15 diff --git a/bin/config.d/lora-rpi-BQ-SG3-BQ35LORA900V1M-slot1.yaml b/bin/config.d/lora-rpi-BQ-SG3-BQ35LORA900V1M-slot1.yaml new file mode 100644 index 00000000000..d58889517d7 --- /dev/null +++ b/bin/config.d/lora-rpi-BQ-SG3-BQ35LORA900V1M-slot1.yaml @@ -0,0 +1,17 @@ +# Station G3 + BQ35LORA900V1M Primary Slot +# Board Doc: https://wiki.bqvoy.com/en/devkits/station-g3 +Meta: + name: B&Q Station G3 + BQ35LORA900V1M Primary Slot + support: official + compatible: + - raspberry-pi + +Lora: + Module: sx1262 + IRQ: 22 + Reset: 16 + Busy: 24 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true + spidev: spidev0.0 + #CS: 8 diff --git a/bin/config.d/lora-usb-meshtoad-e22.yaml b/bin/config.d/lora-usb-meshtoad-e22.yaml index 49182c83e2f..3d8b15783c6 100644 --- a/bin/config.d/lora-usb-meshtoad-e22.yaml +++ b/bin/config.d/lora-usb-meshtoad-e22.yaml @@ -1,3 +1,5 @@ +# This config works with all revisions of the Meshtoad USB radio. + Meta: name: meshtoad-e22 support: official diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index ed5338af647..bdec48f6955 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,15 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.27 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.26 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.25 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.24 diff --git a/boards/heltec_mesh_node_t1.json b/boards/heltec_mesh_node_t1.json new file mode 100644 index 00000000000..96a28da69f2 --- /dev/null +++ b/boards/heltec_mesh_node_t1.json @@ -0,0 +1,54 @@ +{ + "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-n5262", + "mcu": "nrf52840", + "variant": "heltec_mesh_node_t1", + "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 Mesh Node T1", + "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" +} diff --git a/boards/mesh-tracker-x1.json b/boards/mesh-tracker-x1.json new file mode 100644 index 00000000000..5a7dce86b3e --- /dev/null +++ b/boards/mesh-tracker-x1.json @@ -0,0 +1,60 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_WIO_WM1110 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"], + ["0x2886", "0x0057"] + ], + "usb_product": "X1-BOOT", + "mcu": "nrf52840", + "variant": "Seeed_Mesh-Tracker-X1", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "Seeed Mesh Tracker X1", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.seeedstudio.com/SenseCAP-MeshTracker-X1-for-Meshtastic-p-6793.html", + "vendor": "Seeed Studio" +} diff --git a/boards/seeed_wio_tracker_L1_Pro_1W.json b/boards/seeed_wio_tracker_L1_Pro_1W.json new file mode 100644 index 00000000000..f87074a01e9 --- /dev/null +++ b/boards/seeed_wio_tracker_L1_Pro_1W.json @@ -0,0 +1,57 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x2886", "0x1668"], + ["0x2886", "0x1667"] + ], + "usb_product": "TRACKER L1 Pro 1W", + "mcu": "nrf52840", + "variant": "seeed_wio_tracker_L1_Pro_1W", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "seeed_wio_tracker_L1_Pro_1W", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.seeedstudio.com/Wio-Tracker-L1-Pro-p-6454.html", + "vendor": "Seeed Studio" +} diff --git a/boards/t-impulse-plus.json b/boards/t-impulse-plus.json new file mode 100644 index 00000000000..83b289b4224 --- /dev/null +++ b/boards/t-impulse-plus.json @@ -0,0 +1,53 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_T_IMPULSE_PLUS -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [["0x239A", "0x8029"]], + "usb_product": "T-Impulse-Plus-nRF52840", + "mcu": "nrf52840", + "variant": "t-impulse-plus", + "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" + }, + "frameworks": ["arduino"], + "name": "Lilygo T-Impulse-Plus-nRF52840", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "require_upload_port": true, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ] + }, + "url": "https://www.lilygo.cc/", + "vendor": "Lilygo" +} diff --git a/debian/changelog b/debian/changelog index 6b9d0668efd..79425509c72 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,21 @@ +meshtasticd (2.7.27.0) unstable; urgency=medium + + * Version 2.7.27 + + -- GitHub Actions Wed, 24 Jun 2026 11:20:05 +0000 + +meshtasticd (2.7.26.0) unstable; urgency=medium + + * Version 2.7.26 + + -- GitHub Actions Wed, 10 Jun 2026 00:19:23 +0000 + +meshtasticd (2.7.25.0) unstable; urgency=medium + + * Version 2.7.25 + + -- GitHub Actions Sat, 23 May 2026 01:16:20 +0000 + meshtasticd (2.7.24.0) unstable; urgency=medium * Version 2.7.24 diff --git a/debian/ci_pack_sdeb.sh b/debian/ci_pack_sdeb.sh index d35aeef24e3..9aa01825708 100755 --- a/debian/ci_pack_sdeb.sh +++ b/debian/ci_pack_sdeb.sh @@ -33,5 +33,5 @@ if [[ -n $GPG_KEY_ID ]]; then debuild -S -nc -k"$GPG_KEY_ID" else # Build the source deb without signing (forks) - debuild -S -nc + debuild -S -nc -us -uc fi diff --git a/platformio.ini b/platformio.ini index d872780e528..b6f2a01a62e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -68,7 +68,7 @@ monitor_speed = 115200 monitor_filters = direct lib_deps = # renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master - https://github.com/meshtastic/esp8266-oled-ssd1306/archive/6bfd1f135e1ebe37afd6050bb4b9964cea3fcfda.zip + https://github.com/meshtastic/esp8266-oled-ssd1306/archive/2e26010040e028baee72e2093402fa7b3c59e430.zip # renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip # renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master @@ -121,12 +121,12 @@ lib_deps = [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 + https://github.com/jgromes/RadioLib/archive/510e00cfb05bbc3c2b7b524262785454944adb6e.zip [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/4bf593a82100b911ff816dddf7158ffdee2114cd.zip + https://github.com/meshtastic/device-ui/archive/1c45ebc7433acb8ba3fe96a6f7deca9c43fa54cf.zip ; Common libs for environmental measurements in telemetry module [environmental_base] @@ -140,7 +140,7 @@ lib_deps = # renovate: datasource=github-tags depName=NeoPixel packageName=adafruit/Adafruit_NeoPixel https://github.com/adafruit/Adafruit_NeoPixel/archive/1.15.5.zip # renovate: datasource=github-tags depName=Adafruit SSD1306 packageName=adafruit/Adafruit_SSD1306 - https://github.com/adafruit/Adafruit_SSD1306/archive/refs/tags/2.5.16.zip + https://github.com/adafruit/Adafruit_SSD1306/archive/2.5.17.zip # renovate: datasource=github-tags depName=Adafruit BMP280 packageName=adafruit/Adafruit_BMP280_Library https://github.com/adafruit/Adafruit_BMP280_Library/archive/refs/tags/3.0.0.zip # renovate: datasource=github-tags depName=Adafruit BMP085 packageName=adafruit/Adafruit-BMP085-Library @@ -185,16 +185,24 @@ lib_deps = https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library/archive/refs/tags/v1.1.2.zip # renovate: datasource=github-tags depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/SparkFun_ICM-20948_ArduinoLibrary https://github.com/sparkfun/SparkFun_ICM-20948_ArduinoLibrary/archive/refs/tags/v1.3.2.zip + # renovate: datasource=github-tags depName=TDK InvenSense ICM42670P packageName=tdk-invn-oss/motion.arduino.ICM42670P + https://github.com/tdk-invn-oss/motion.arduino.ICM42670P/archive/refs/tags/1.0.8.zip # renovate: datasource=github-tags depName=Adafruit LTR390 Library packageName=adafruit/Adafruit_LTR390 https://github.com/adafruit/Adafruit_LTR390/archive/refs/tags/1.1.2.zip # renovate: datasource=github-tags depName=Adafruit PCT2075 packageName=adafruit/Adafruit_PCT2075 https://github.com/adafruit/Adafruit_PCT2075/archive/refs/tags/1.0.6.zip # renovate: datasource=github-tags depName=DFRobot_BMM150 packageName=dfrobot/DFRobot_BMM150 https://github.com/DFRobot/DFRobot_BMM150/archive/refs/tags/V1.0.0.zip + # renovate: datasource=github-tags depName=SparkFun MMC5983MA Magnetometer packageName=sparkfun/SparkFun_MMC5983MA_Magnetometer_Arduino_Library + https://github.com/sparkfun/SparkFun_MMC5983MA_Magnetometer_Arduino_Library/archive/v1.1.5.zip # renovate: datasource=github-tags depName=Adafruit_TSL2561 packageName=adafruit/Adafruit_TSL2561 https://github.com/adafruit/Adafruit_TSL2561/archive/refs/tags/1.1.3.zip # renovate: datasource=github-tags depName=BH1750_WE packageName=wollewald/BH1750_WE https://github.com/wollewald/BH1750_WE/archive/refs/tags/1.1.10.zip + # renovate: datasource=github-tags depName=Adafruit SHTC3 packageName=adafruit/Adafruit_SHTC3 + https://github.com/adafruit/Adafruit_SHTC3/archive/refs/tags/1.0.2.zip + # renovate: datasource=github-tags depName=SPA06_003 packageName=adafruit/Adafruit_SPA06_003 + https://github.com/adafruit/Adafruit_SPA06_003/archive/refs/tags/1.0.2.zip ; Common environmental sensor libraries (not included in native / portduino) [environmental_extra_common] @@ -203,8 +211,6 @@ lib_deps = https://github.com/adafruit/Adafruit_BMP3XX/archive/refs/tags/2.1.6.zip # renovate: datasource=github-tags depName=Adafruit MAX1704X packageName=adafruit/Adafruit_MAX1704X https://github.com/adafruit/Adafruit_MAX1704X/archive/refs/tags/1.0.3.zip - # renovate: datasource=github-tags depName=Adafruit SHTC3 packageName=adafruit/Adafruit_SHTC3 - https://github.com/adafruit/Adafruit_SHTC3/archive/refs/tags/1.0.2.zip # renovate: datasource=github-tags depName=Adafruit LPS2X packageName=adafruit/Adafruit_LPS2X https://github.com/adafruit/Adafruit_LPS2X/archive/refs/tags/2.0.6.zip # renovate: datasource=github-tags depName=Adafruit SHT31 packageName=adafruit/Adafruit_SHT31 @@ -226,7 +232,7 @@ lib_deps = # renovate: datasource=github-tags depName=Sensirion I2C SFA3x packageName=sensirion/arduino-i2c-sfa3x https://github.com/Sensirion/arduino-i2c-sfa3x/archive/refs/tags/1.0.0.zip # renovate: datasource=github-tags depName=Sensirion I2C SCD30 packageName=sensirion/arduino-i2c-scd30 - https://github.com/Sensirion/arduino-i2c-scd30/archive/refs/tags/1.0.0.zip + https://github.com/Sensirion/arduino-i2c-scd30/archive/1.1.1.zip ; Environmental sensors with BSEC2 (Bosch proprietary IAQ) [environmental_extra] diff --git a/protobufs b/protobufs index 59cb394dcfc..cf0a84ede1e 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 59cb394dcfc4432cb216358ca26e861c7d13f462 +Subproject commit cf0a84ede1e7a0b7479e90a5e496d30c5dfba707 diff --git a/src/AudioThread.h b/src/AudioThread.h index 1129ee087ec..3a44bf82396 100644 --- a/src/AudioThread.h +++ b/src/AudioThread.h @@ -12,11 +12,18 @@ #include #include +// A board with an I2S amplifier opts in by defining AUDIO_AMP_ENABLE(on) in its variant.h to power the +// amp on/off around playback (e.g. an enable pin on an I/O expander). The includes below expose the +// expander instances (io / mcpIoExpander) those macros typically reference. #ifdef USE_XL9555 #include "ExtensionIOXL9555.hpp" extern ExtensionIOXL9555 io; #endif +#ifdef USE_MCP23017 +#include "platform/esp32/ExtensionIOMCP23017.h" +#endif + #define AUDIO_THREAD_INTERVAL_MS 100 class AudioThread : public concurrency::OSThread @@ -26,8 +33,8 @@ class AudioThread : public concurrency::OSThread void beginRttl(const void *data, uint32_t len) { -#ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, HIGH); +#ifdef AUDIO_AMP_ENABLE + AUDIO_AMP_ENABLE(true); #endif setCPUFast(true); rtttlFile = std::unique_ptr(new AudioFileSourcePROGMEM(data, len)); @@ -54,8 +61,8 @@ class AudioThread : public concurrency::OSThread rtttlFile = nullptr; setCPUFast(false); -#ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, LOW); +#ifdef AUDIO_AMP_ENABLE + AUDIO_AMP_ENABLE(false); #endif } @@ -66,14 +73,14 @@ class AudioThread : public concurrency::OSThread i2sRtttl = nullptr; } -#ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, HIGH); +#ifdef AUDIO_AMP_ENABLE + AUDIO_AMP_ENABLE(true); #endif auto sam = std::unique_ptr(new ESP8266SAM); sam->Say(audioOut.get(), text); setCPUFast(false); -#ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, LOW); +#ifdef AUDIO_AMP_ENABLE + AUDIO_AMP_ENABLE(false); #endif } diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp index f215be80fb6..cbeb98d531b 100644 --- a/src/FSCommon.cpp +++ b/src/FSCommon.cpp @@ -99,48 +99,87 @@ bool renameFile(const char *pathFrom, const char *pathTo) #endif } +#include +#include +#include #include -/** - * @brief Get the list of files in a directory. - * - * This function returns a list of files in a directory. The list includes the full path of each file. - * We can't use SPILOCK here because of recursion. Callers of this function should use SPILOCK. - * - * @param dirname The name of the directory. - * @param levels The number of levels of subdirectories to list. - * @return A vector of strings containing the full path of each file in the directory. - */ -std::vector getFiles(const char *dirname, uint8_t levels) -{ - std::vector filenames = {}; #ifdef FSCom +namespace +{ +bool pathEndsWithDot(const char *path) +{ + if (!path) + return false; + + size_t length = strlen(path); + return length > 0 && path[length - 1] == '.'; +} + +bool copyFilePath(char *dest, size_t destSize, const char *path, bool *wasLimited) +{ + if (!path || destSize == 0) { + if (wasLimited) + *wasLimited = true; + return false; + } + + if (strlcpy(dest, path, destSize) >= destSize) { + if (wasLimited) + *wasLimited = true; + return false; + } + + return true; +} + +void collectFiles(const char *dirname, uint8_t levels, size_t maxCount, std::vector &filenames, + bool *wasLimited) +{ + if (!dirname) + return; + File root = FSCom.open(dirname, FILE_O_READ); if (!root) - return filenames; - if (!root.isDirectory()) - return filenames; + return; + if (!root.isDirectory()) { + root.close(); + return; + } File file = root.openNextFile(); - while (file) { - if (file.isDirectory() && !String(file.name()).endsWith(".")) { - if (levels) { + // file.name()[0] check is a workaround for a bug in the Adafruit LittleFS nrf52 glue (see issue 4395) + while (file && file.name()[0]) { + if (filenames.size() >= maxCount) { + if (wasLimited) + *wasLimited = true; + file.close(); + break; + } + const char *fileName = file.name(); + if (file.isDirectory() && !pathEndsWithDot(fileName)) { + char pathBuffer[sizeof(((meshtastic_FileInfo *)nullptr)->file_name)] = {}; #ifdef ARCH_ESP32 - std::vector subDirFilenames = getFiles(file.path(), levels - 1); + const char *subDirPath = file.path(); #else - std::vector subDirFilenames = getFiles(file.name(), levels - 1); + const char *subDirPath = fileName; #endif - filenames.insert(filenames.end(), subDirFilenames.begin(), subDirFilenames.end()); - file.close(); + bool hasSubDirPath = copyFilePath(pathBuffer, sizeof(pathBuffer), subDirPath, wasLimited); + file.close(); + + if (levels && hasSubDirPath) { + collectFiles(pathBuffer, levels - 1, maxCount, filenames, wasLimited); + } else if (wasLimited) { + *wasLimited = true; } } else { meshtastic_FileInfo fileInfo = {"", static_cast(file.size())}; #ifdef ARCH_ESP32 - strcpy(fileInfo.file_name, file.path()); + bool hasFilePath = copyFilePath(fileInfo.file_name, sizeof(fileInfo.file_name), file.path(), wasLimited); #else - strcpy(fileInfo.file_name, file.name()); + bool hasFilePath = copyFilePath(fileInfo.file_name, sizeof(fileInfo.file_name), file.name(), wasLimited); #endif - if (!String(fileInfo.file_name).endsWith(".")) { + if (hasFilePath && !pathEndsWithDot(fileInfo.file_name)) { filenames.push_back(fileInfo); } file.close(); @@ -148,6 +187,41 @@ std::vector getFiles(const char *dirname, uint8_t levels) file = root.openNextFile(); } root.close(); +} +} // namespace +#endif + +// Callers must hold the SPI lock; recursion prevents taking it here. +std::vector getFiles(const char *dirname, uint8_t levels, size_t maxCount, bool *wasLimited) +{ + std::vector filenames = {}; + if (wasLimited) + *wasLimited = false; +#ifdef FSCom +#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) + size_t reservedCount = maxCount; + while (reservedCount > 0) { + try { + filenames.reserve(reservedCount); + break; + } catch (const std::bad_alloc &) { + reservedCount /= 2; + } catch (const std::length_error &) { + reservedCount /= 2; + } + } + if (reservedCount == 0) { + if (wasLimited) + *wasLimited = true; + return filenames; + } + if (reservedCount < maxCount) { + if (wasLimited) + *wasLimited = true; + maxCount = reservedCount; + } +#endif + collectFiles(dirname, levels, maxCount, filenames, wasLimited); #endif return filenames; } @@ -335,4 +409,4 @@ void setupSDCard() LOG_DEBUG("Total space: %lu MB", (uint32_t)(SD.totalBytes() / (1024 * 1024))); LOG_DEBUG("Used space: %lu MB", (uint32_t)(SD.usedBytes() / (1024 * 1024))); #endif -} \ No newline at end of file +} diff --git a/src/FSCommon.h b/src/FSCommon.h index fdc0b76ecd1..c85c07962d0 100644 --- a/src/FSCommon.h +++ b/src/FSCommon.h @@ -52,7 +52,7 @@ void fsInit(); void fsListFiles(); bool copyFile(const char *from, const char *to); bool renameFile(const char *pathFrom, const char *pathTo); -std::vector getFiles(const char *dirname, uint8_t levels); +std::vector getFiles(const char *dirname, uint8_t levels, size_t maxCount = 64, bool *wasLimited = nullptr); void listDir(const char *dirname, uint8_t levels, bool del = false); void rmDir(const char *dirname); -void setupSDCard(); \ No newline at end of file +void setupSDCard(); diff --git a/src/MessageStore.cpp b/src/MessageStore.cpp index 22da418f524..73e1de74205 100644 --- a/src/MessageStore.cpp +++ b/src/MessageStore.cpp @@ -362,7 +362,14 @@ void MessageStore::clearAllMessages() #ifdef FSCom SafeFile f(filename.c_str(), false); uint8_t count = 0; - f.write(&count, 1); // write "0 messages" + + // SafeFile already does its own spiLock in its constructor and close(). + // Avoid nesting spiLocks, as this will hang until watchdog reset! + { + concurrency::LockGuard guard(spiLock); + f.write(&count, 1); // write "0 messages" + } + f.close(); #endif diff --git a/src/Power.cpp b/src/Power.cpp index f752e9461df..ef1143d0218 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -23,6 +23,7 @@ #include "main.h" #include "meshUtils.h" #include "power/PowerHAL.h" +#include "power/SGM41562.h" #include "sleep.h" #if defined(ARCH_PORTDUINO) @@ -453,6 +454,10 @@ class AnalogBatteryLevel : public HasBatteryLevel /// source virtual bool isVbusIn() override { +#ifdef HAS_SGM41562 + if (sgm41562 && sgm41562->refresh()) + return sgm41562->isInputPowerGood(); +#endif #ifdef EXT_PWR_DETECT #if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB) // if external powered that pin will be pulled down @@ -483,6 +488,10 @@ class AnalogBatteryLevel : public HasBatteryLevel /// we can't be smart enough to say 'full'? virtual bool isCharging() override { +#ifdef HAS_SGM41562 + if (sgm41562 && sgm41562->refresh()) + return sgm41562->isCharging(); +#endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) && !defined(HAS_PMU) if (hasRAK()) { return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse; @@ -697,6 +706,12 @@ bool Power::analogInit() */ bool Power::setup() { +#ifdef HAS_SGM41562 + // Initialize the charger early so AnalogBatteryLevel can read charging + // state from it. The charger does not provide battery voltage / percent — + // those still come from the platform ADC via analogInit() below. + initSGM41562(SGM41562_WIRE); +#endif bool found = false; if (axpChipInit()) { found = true; diff --git a/src/configuration.h b/src/configuration.h index 2c084174d00..180f2dc8c4c 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -31,6 +31,11 @@ along with this program. If not, see . #endif #if __has_include("SensorRtcHelper.hpp") #include "SensorRtcHelper.hpp" +// SensorLib defines isBitSet as a macro; undefine it here to avoid conflicts +// with the SparkFun MMC5983MA library, which has a class method of the same name. +#ifdef isBitSet +#undef isBitSet +#endif #endif /* Offer chance for variant-specific defines */ @@ -174,6 +179,13 @@ along with this program. If not, see . #define TX_GAIN_LORA 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8 #endif +#ifdef SEEED_WIO_TRACKER_L1_PRO_1W +// Re-indexed from Seeed's 31-point table (based at -9 dBm) to limitPower()'s 0-based radio dBm. +// TODO: verify against measured output. +#define NUM_PA_POINTS 22 +#define TX_GAIN_LORA 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10 +#endif + // Default system gain to 0 if not defined #ifndef NUM_PA_POINTS #define NUM_PA_POINTS 1 @@ -203,7 +215,7 @@ along with this program. If not, see . #define SSD1306_ADDRESS_L 0x3C // Addr = 0 #define SSD1306_ADDRESS_H 0x3D // Addr = 1 -#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) +#if (defined(SEEED_WIO_TRACKER_L1) || defined(SEEED_WIO_TRACKER_L1_PRO_1W)) && !defined(SEEED_WIO_TRACKER_L1_EINK) #define SSD1306_ADDRESS SSD1306_ADDRESS_H #define USE_SH1106 #endif @@ -238,6 +250,7 @@ along with this program. If not, see . #define QMI8658_ADDR 0x6B #define QMC5883L_ADDR 0x0D #define HMC5883L_ADDR 0x1E +#define MMC5983MA_ADDR 0x30 #define SHTC3_ADDR 0x70 #define LPS22HB_ADDR 0x5C #define LPS22HB_ADDR_ALT 0x5D @@ -287,6 +300,8 @@ along with this program. If not, see . #define DA217_ADDR 0x26 #define BMI270_ADDR 0x68 #define BMI270_ADDR_ALT 0x69 +#define ICM42607P_ADDR 0x68 +#define ICM42607P_ADDR_ALT 0x69 // ----------------------------------------------------------------------------- // LED diff --git a/src/detect/LoRaRadioType.h b/src/detect/LoRaRadioType.h index a059a3668be..a66a40d3477 100644 --- a/src/detect/LoRaRadioType.h +++ b/src/detect/LoRaRadioType.h @@ -11,7 +11,8 @@ enum LoRaRadioType { SX1280_RADIO, LR1110_RADIO, LR1120_RADIO, - LR1121_RADIO + LR1121_RADIO, + LR2021_RADIO, }; extern LoRaRadioType radioType; \ No newline at end of file diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index 75eabc9541d..aa25ab5e117 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -37,8 +37,15 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const { - ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150, BMI270}; - return firstOfOrNONE(10, types); + ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, + ICM20948, QMA6100P, BMM150, BMI270, ICM42607P}; + return firstOfOrNONE(11, types); +} + +ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const +{ + ScanI2C::DeviceType types[] = {MMC5983MA}; + return firstOfOrNONE(1, types); } ScanI2C::FoundDevice ScanI2C::firstAQI() const diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 054c7854baf..f0d60cb5f2b 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -41,6 +41,7 @@ class ScanI2C QMI8658, QMC5883L, HMC5883L, + MMC5983MA, PMSA003I, QMA6100P, MPU6050, @@ -65,6 +66,7 @@ class ScanI2C FT6336U, STK8BAXX, ICM20948, + ICM42607P, SCD4X, MAX30102, TPS65233, @@ -97,6 +99,7 @@ class ScanI2C CW2015, SCD30, ADS1115, + SPA06, } DeviceType; // typedef uint8_t DeviceAddress; @@ -149,6 +152,8 @@ class ScanI2C FoundDevice firstAccelerometer() const; + FoundDevice firstMagnetometer() const; + FoundDevice firstAQI() const; FoundDevice firstRGBLED() const; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index e992fb276d3..e515b46b08f 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -85,8 +85,9 @@ ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const return o_probe; } + uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation ®isterLocation, - ScanI2CTwoWire::ResponseWidth responseWidth, bool zeropad = false) const + ScanI2CTwoWire::ResponseWidth responseWidth, bool zeropad) const { uint16_t value = 0x00; TwoWire *i2cBus = fetchI2CBus(registerLocation.i2cAddress); @@ -175,6 +176,62 @@ String readSEN5xProductName(TwoWire *i2cBus, uint8_t address) return String(productName); } +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +static uint8_t crcSHT2X(const uint8_t *data, uint8_t len) +{ + uint8_t crc = 0; + for (uint8_t i = 0; i < len; i++) { + crc ^= data[i]; + for (uint8_t bit = 0; bit < 8; bit++) { + crc = (crc & 0x80) ? (crc << 1) ^ 0x31 : crc << 1; + } + } + return crc; +} + +bool detectSHT21SerialNumber(TwoWire *i2cBus, uint8_t address) +{ + uint8_t serialA[8] = {0}; + uint8_t serialB[6] = {0}; + + i2cBus->beginTransmission(address); + i2cBus->write(0xFA); + i2cBus->write(0x0F); + + if (i2cBus->endTransmission() != 0) + return false; + + if (i2cBus->requestFrom(address, (uint8_t)sizeof(serialA)) != sizeof(serialA)) + return false; + + for (uint8_t i = 0; i < sizeof(serialA); i++) { + if (!i2cBus->available()) + return false; + serialA[i] = i2cBus->read(); + } + + i2cBus->beginTransmission(address); + i2cBus->write(0xFC); + i2cBus->write(0xC9); + + if (i2cBus->endTransmission() != 0) + return false; + + if (i2cBus->requestFrom(address, (uint8_t)sizeof(serialB)) != sizeof(serialB)) + return false; + + for (uint8_t i = 0; i < sizeof(serialB); i++) { + if (!i2cBus->available()) + return false; + serialB[i] = i2cBus->read(); + } + + return crcSHT2X(&serialA[0], 1) == serialA[1] && crcSHT2X(&serialA[2], 1) == serialA[3] && + crcSHT2X(&serialA[4], 1) == serialA[5] && crcSHT2X(&serialA[6], 1) == serialA[7] && + crcSHT2X(&serialB[0], 2) == serialB[2] && crcSHT2X(&serialB[3], 2) == serialB[5]; +} +#endif + #define SCAN_SIMPLE_CASE(ADDR, T, ...) \ case ADDR: \ logFoundDevice(__VA_ARGS__); \ @@ -301,9 +358,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address); #ifdef HAS_NCP5623 SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623", (uint8_t)addr.address); -#endif -#ifdef HAS_LP5562 - SCAN_SIMPLE_CASE(LP5562_ADDR, LP5562, "LP5562", (uint8_t)addr.address); #endif case XPOWERS_AXP192_AXP2101_ADDRESS: // Do we have the axp2101/192 or the TCA8418 @@ -340,8 +394,12 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) logFoundDevice("DPS310", (uint8_t)addr.address); type = DPS310; break; + case 0x11: + logFoundDevice("SPA06-003", (uint8_t)addr.address); + type = SPA06; + break; } - if (type == DPS310) { + if (type == DPS310 || type == SPA06) { break; } default: @@ -524,6 +582,18 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) #else SCAN_SIMPLE_CASE(PMSA003I_ADDR, PMSA003I, "PMSA003I", (uint8_t)addr.address) #endif + case MMC5983MA_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x2F), 1); + if (registerValue == 0x30) { + type = MMC5983MA; + logFoundDevice("MMC5983MA", (uint8_t)addr.address); +#ifdef HAS_LP5562 + } else { + type = LP5562; + logFoundDevice("LP5562", (uint8_t)addr.address); +#endif + } + break; case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333 @@ -678,8 +748,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) } break; - case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT, and SEN5X_ADDR - case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR + case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT, ICM42607P_ADDR_ALT, and SEN5X_ADDR + case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR, and ICM42607P_ADDR registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); #ifdef HAS_ICM20948 type = ICM20948; @@ -699,6 +769,12 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) logFoundDevice("BMX160", (uint8_t)addr.address); break; } else { + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x75), 1); // WHO_AM_I + if (registerValue == 0x60) { + type = ICM42607P; + logFoundDevice("ICM-42607-P", (uint8_t)addr.address); + break; + } String prod = ""; prod = readSEN5xProductName(i2cBus, addr.address); if (prod.startsWith("SEN55")) { diff --git a/src/detect/ScanI2CTwoWire.h b/src/detect/ScanI2CTwoWire.h index 841a8b946c4..7fb2e8b5a8b 100644 --- a/src/detect/ScanI2CTwoWire.h +++ b/src/detect/ScanI2CTwoWire.h @@ -53,7 +53,7 @@ class ScanI2CTwoWire : public ScanI2C concurrency::Lock lock; - uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool) const; + uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool = false) const; bool i2cCommandResponseLength(DeviceAddress addr, uint16_t command, uint8_t expectedLength) const; diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 3bc047ad806..69b5b7199ee 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -832,7 +832,10 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) switch (newState) { case GPS_ACTIVE: case GPS_IDLE: - if (oldState == GPS_ACTIVE || oldState == GPS_IDLE) // If hardware already awake, no changes needed + if (oldState == GPS_ACTIVE) + break; + gotTime = false; + if (oldState == GPS_IDLE) // If hardware already awake, no changes needed break; if (oldState != GPS_ACTIVE && oldState != GPS_IDLE) // If hardware just waking now, clear buffer clearBuffer(); @@ -1142,8 +1145,7 @@ int32_t GPS::runOnce() // if gps_update_interval is <=10s, GPS never goes off, so we treat that differently uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval); - // 1. Got a time for the first time - bool gotTime = (getRTCQuality() >= RTCQualityGPS); + // 1. Got a time for the first time this cycle if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time gotTime = true; } diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 8d63ce82fee..6da558b81c5 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -163,6 +163,7 @@ class GPS : private concurrency::OSThread uint32_t lastChecksumFailCount = 0; uint8_t currentStep = 0; int32_t currentDelay = 2000; + bool gotTime = false; #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS // (20210908) TinyGps++ can only read the GPGSA "FIX TYPE" field diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index a8288a069ad..ad0bdec0407 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -31,13 +31,63 @@ static uint32_t timeStartMsec; // Once we have a GPS lock, this is where we hold the initial msec clock that corresponds to that time static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only updated once on initial lock +#ifdef PIO_UNIT_TESTING +// Test seam: unit tests can inject a fake system clock (e.g. the uptime seconds that +// gettimeofday() returns on boards without a real RTC, like RP2040) and force readFromRTC() +// down the no-hardware-RTC fallback even when a hardware-RTC branch is compiled in. +static bool hasMockSystemTime = false; +static bool forceSystemTimeFallback = false; +static struct timeval mockSystemTime = {}; +#endif + +// Reads the platform system clock (or the injected mock during unit tests). Used only by the +// no-hardware-RTC fallback below, so it may be unused on builds with a hardware RTC. +[[maybe_unused]] static bool readSystemTime(struct timeval *tv) +{ +#ifdef PIO_UNIT_TESTING + if (hasMockSystemTime) { + *tv = mockSystemTime; + return true; + } +#endif + return gettimeofday(tv, NULL) == 0; +} + +// Seeds the clock from the system time on boards without a hardware RTC. gettimeofday() can +// return uptime rather than wall-clock time there (e.g. RP2040), so only adopt it when we have +// nothing better yet -- never clobber a higher-quality GPS/NTP/phone source (issue #9828). +[[maybe_unused]] static RTCSetResult readFromSystemTimeFallback() +{ + struct timeval tv; + if (readSystemTime(&tv)) { + uint32_t now = millis(); + uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + if (currentQuality == RTCQualityNone) { + LOG_DEBUG("Seed time from system clock: %lu", (unsigned long)printableEpoch); + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; + } else { + LOG_DEBUG("Ignore system clock fallback (%lu); current RTC quality is %s", (unsigned long)printableEpoch, + RtcName(currentQuality)); + } + return RTCSetResultSuccess; + } + return RTCSetResultNotSet; +} + /** - * Reads the current date and time from the RTC module and updates the system time. - * @return True if the RTC was successfully read and the system time was updated, false otherwise. + * Reads date/time from the RTC module (or system-time fallback) and seeds internal timekeeping. + * @return RTCSetResultSuccess if a time source was read successfully (even if an existing higher-quality time is retained). */ RTCSetResult readFromRTC() { - struct timeval tv; /* btw settimeofday() is helpful here too*/ +#ifdef PIO_UNIT_TESTING + if (forceSystemTimeFallback) { + return readFromSystemTimeFallback(); + } +#endif + + [[maybe_unused]] struct timeval tv; /* btw settimeofday() is helpful here too*/ #ifdef RV3028_RTC if (rtc_found.address == RV3028_RTC) { uint32_t now = millis(); @@ -162,14 +212,7 @@ RTCSetResult readFromRTC() } } #else - if (!gettimeofday(&tv, NULL)) { - uint32_t now = millis(); - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms - LOG_DEBUG("Read RTC time as %ld", printableEpoch); - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; - return RTCSetResultSuccess; - } + return readFromSystemTimeFallback(); #endif return RTCSetResultNotSet; } @@ -219,8 +262,8 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd } else if (q == RTCQualityGPS) { shouldSet = true; LOG_DEBUG("Reapply GPS time: %ld secs", printableEpoch); - } else if (q == RTCQualityNTP && !Throttle::isWithinTimespanMs(lastSetMsec, (12 * 60 * 60 * 1000UL))) { - // Every 12 hrs we will slam in a new NTP or Phone GPS / NTP time, to correct for local RTC clock drift + } else if (q == RTCQualityNTP && !Throttle::isWithinTimespanMs(lastSetMsec, (30 * 60 * 1000UL))) { + // Every 30 minutes we will slam in a new NTP or Phone GPS / NTP time, to correct for local RTC clock drift shouldSet = true; LOG_DEBUG("Reapply external time to correct clock drift %ld secs", printableEpoch); } else { @@ -292,7 +335,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd LOG_WARN("Failed to set time for RX8130CE"); } } -#elif defined(ARCH_ESP32) +#elif defined(ARCH_ESP32) || defined(ARCH_RP2040) settimeofday(tv, NULL); #endif @@ -423,6 +466,38 @@ void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot) lastSetFromPhoneNtpOrGps = 0; lastTimeValidationWarning = 0; } + +void clearRTCSystemTimeForTests() +{ + hasMockSystemTime = false; + mockSystemTime = {}; +} + +void setRTCSystemTimeForTests(const struct timeval *tv) +{ + if (tv == NULL) { + clearRTCSystemTimeForTests(); + return; + } + mockSystemTime = *tv; + hasMockSystemTime = true; +} + +void setReadFromRTCUseSystemTimeForTests(bool enabled) +{ + forceSystemTimeFallback = enabled; +} + +void resetRTCStateForTests() +{ + currentQuality = RTCQualityNone; + timeStartMsec = 0; + zeroOffsetSecs = 0; + lastSetFromPhoneNtpOrGps = 0; + lastTimeValidationWarning = 0; + setReadFromRTCUseSystemTimeForTests(false); + clearRTCSystemTimeForTests(); +} #endif time_t gm_mktime(const struct tm *tm) diff --git a/src/gps/RTC.h b/src/gps/RTC.h index cd1e1d00295..b69a99ec947 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -56,6 +56,10 @@ RTCSetResult readFromRTC(); #ifdef PIO_UNIT_TESTING void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot); +void resetRTCStateForTests(); +void setRTCSystemTimeForTests(const struct timeval *tv); +void clearRTCSystemTimeForTests(); +void setReadFromRTCUseSystemTimeForTests(bool enabled); #endif time_t gm_mktime(const struct tm *tm); diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index f51a6ee9e01..39de336ea9c 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -60,6 +60,7 @@ along with this program. If not, see . #include "main.h" #include "mesh-pb-constants.h" #include "mesh/Channels.h" +#include "mesh/Default.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" @@ -98,6 +99,7 @@ namespace graphics // This means the *visible* area (sh1106 can address 132, but shows 128 for example) #define IDLE_FRAMERATE 1 // in fps +#define COMPASS_ACTIVE_FRAMERATE 20 // DEBUG #define NUM_EXTRA_FRAMES 3 // text message and debug frame @@ -135,6 +137,60 @@ static bool heartbeat = false; extern bool hasUnreadMessage; +static inline float wrapHeading360(float heading) +{ + if (heading < 0.0f) { + heading += 360.0f; + } else if (heading >= 360.0f) { + heading -= 360.0f; + } + return heading; +} + +void Screen::setHeading(float heading) +{ + const float wrappedHeading = wrapHeading360(heading); + + if (!hasCompass) { + hasCompass = true; + compassHeading = wrappedHeading; + return; + } + + // Interpolate using shortest-path angular delta to avoid jumps around 0/360. + float delta = wrappedHeading - compassHeading; + if (delta > 180.0f) { + delta -= 360.0f; + } else if (delta < -180.0f) { + delta += 360.0f; + } + + // Adaptive filtering: + // - Strong damping for tiny deltas (jitter) + // - Faster response for larger turns + const float absDelta = (delta >= 0.0f) ? delta : -delta; + if (absDelta < 1.0f) { + return; + } + + float alpha = 0.35f; + if (absDelta > 25.0f) { + alpha = 0.85f; + } else if (absDelta > 10.0f) { + alpha = 0.65f; + } + + float step = delta * alpha; + const float maxStep = 12.0f; + if (step > maxStep) { + step = maxStep; + } else if (step < -maxStep) { + step = -maxStep; + } + + compassHeading = wrapHeading360(compassHeading + step); +} + // ============================== // Overlay Alert Banner Renderer // ============================== @@ -272,10 +328,25 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int float Screen::estimatedHeading(double lat, double lon) { static double oldLat, oldLon; - static float b; + static float b = -1.0f; + static uint32_t lastHeadingAtMs = 0; + const uint32_t now = millis(); + const uint32_t gpsUpdateIntervalSecs = + Default::getConfiguredOrDefault(config.position.gps_update_interval, default_gps_update_interval); + uint32_t effectiveUpdateIntervalSecs = gpsUpdateIntervalSecs; + if (config.position.position_broadcast_smart_enabled) { + const uint32_t smartMinIntervalSecs = Default::getConfiguredOrDefault( + config.position.broadcast_smart_minimum_interval_secs, default_broadcast_smart_minimum_interval_secs); + if (smartMinIntervalSecs > effectiveUpdateIntervalSecs) { + effectiveUpdateIntervalSecs = smartMinIntervalSecs; + } + } + // Two expected update windows; keep arithmetic 32-bit to avoid pulling in larger 64-bit helpers. + const uint32_t headingStaleMs = + (effectiveUpdateIntervalSecs > (UINT32_MAX / 2000U)) ? UINT32_MAX : (effectiveUpdateIntervalSecs * 2000U); if (oldLat == 0) { - // just prepare for next time + // Need at least two position points before we can infer heading. oldLat = lat; oldLon = lon; @@ -283,12 +354,20 @@ float Screen::estimatedHeading(double lat, double lon) } float d = GeoCoord::latLongToMeter(oldLat, oldLon, lat, lon); - if (d < 10) // haven't moved enough, just keep current bearing + if (d < 10) { // haven't moved enough, keep previous heading (invalid until first real movement) + if (lastHeadingAtMs != 0 && (now - lastHeadingAtMs) >= headingStaleMs) { + // Heading is stale after prolonged no-movement; force reacquire. + b = -1.0f; + oldLat = lat; + oldLon = lon; + } return b; + } b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG; oldLat = lat; oldLon = lon; + lastHeadingAtMs = now; return b; } @@ -928,9 +1007,22 @@ int32_t Screen::runOnce() // but we should only call setTargetFPS when framestate changes, because // otherwise that breaks animations. - if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) { + uint32_t desiredFramerate = IDLE_FRAMERATE; +#if HAS_GPS && !defined(USE_EINK) + if (showingNormalScreen && hasCompass) { + const uint8_t currentFrame = ui->getUiState()->currentFrame; + if ((framesetInfo.positions.gps != 255 && currentFrame == framesetInfo.positions.gps) || + (framesetInfo.positions.waypoint != 255 && currentFrame == framesetInfo.positions.waypoint) || + (framesetInfo.positions.firstFavorite != 255 && currentFrame >= framesetInfo.positions.firstFavorite && + currentFrame <= framesetInfo.positions.lastFavorite)) { + desiredFramerate = COMPASS_ACTIVE_FRAMERATE; + } + } +#endif + + if (targetFramerate != desiredFramerate && ui->getUiState()->frameState == FIXED) { // oldFrameState = ui->getUiState()->frameState; - targetFramerate = IDLE_FRAMERATE; + targetFramerate = desiredFramerate; ui->setTargetFPS(targetFramerate); forceDisplay(); diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 023f36f3877..401cba59ef7 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -330,15 +330,11 @@ class Screen : public concurrency::OSThread // Function to allow the AccelerometerThread to set the heading if a sensor provides it // Mutex needed? - void setHeading(long _heading) - { - hasCompass = true; - compassHeading = fmod(_heading, 360); - } + void setHeading(float heading); bool hasHeading() { return hasCompass; } - long getHeading() { return compassHeading; } + float getHeading() { return compassHeading; } void setEndCalibration(uint32_t _endCalibrationAt) { endCalibrationAt = _endCalibrationAt; } uint32_t getEndCalibration() { return endCalibrationAt; } @@ -792,4 +788,4 @@ extern std::vector functionSymbol; extern std::string functionSymbolString; extern graphics::Screen *screen; -#endif \ No newline at end of file +#endif diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index b69d7948345..ea700ef3eb9 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -1359,7 +1359,8 @@ void TFTDisplay::sendCommand(uint8_t com) digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON); #elif defined(HACKADAY_COMMUNICATOR) tft->displayOn(); -#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) +#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) && \ + !defined(HELTEC_MESH_NODE_T1) tft->wakeup(); tft->powerSaveOff(); #endif @@ -1370,7 +1371,7 @@ void TFTDisplay::sendCommand(uint8_t com) #ifdef UNPHONE unphone.backlight(true); // using unPhone library #endif -#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1) #elif !defined(M5STACK) && !defined(ST7789_CS) && \ !defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function tft->setBrightness(172); @@ -1386,7 +1387,8 @@ void TFTDisplay::sendCommand(uint8_t com) digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON); #elif defined(HACKADAY_COMMUNICATOR) tft->displayOff(); -#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) +#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) && \ + !defined(HELTEC_MESH_NODE_T1) tft->sleep(); tft->powerSaveOn(); #endif @@ -1397,7 +1399,7 @@ void TFTDisplay::sendCommand(uint8_t com) #ifdef UNPHONE unphone.backlight(false); // using unPhone library #endif -#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1) #elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) tft->setBrightness(0); #endif @@ -1412,7 +1414,7 @@ void TFTDisplay::sendCommand(uint8_t com) void TFTDisplay::setDisplayBrightness(uint8_t _brightness) { -#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1) // todo #elif !defined(HACKADAY_COMMUNICATOR) tft->setBrightness(_brightness); @@ -1432,7 +1434,7 @@ bool TFTDisplay::hasTouch(void) { #ifdef RAK14014 return true; -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && !defined(HELTEC_MESH_NODE_T1) return tft->touch() != nullptr; #else return false; @@ -1451,7 +1453,7 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y) } else { return false; } -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && !defined(HELTEC_MESH_NODE_T1) return tft->getTouch(x, y); #else return false; @@ -1468,7 +1470,7 @@ bool TFTDisplay::connect() { concurrency::LockGuard g(spiLock); LOG_INFO("Do TFT init"); -#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1) tft = new TFT_eSPI; #elif defined(HACKADAY_COMMUNICATOR) bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */); diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp index 52f0195b354..43b33e8538d 100644 --- a/src/graphics/VirtualKeyboard.cpp +++ b/src/graphics/VirtualKeyboard.cpp @@ -142,8 +142,9 @@ void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offset if (keyboardStartY < 0) keyboardStartY = 0; } else { - // Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom - cellH = KEY_HEIGHT; + // Default (non-wide, non-64px) e.g. SH1107 128x128: + // cellH = FONT_HEIGHT_SMALL - 2 so rows are tighter while still hosting the font + cellH = std::max((int)KEY_HEIGHT, FONT_HEIGHT_SMALL - 2); int keyboardHeight = KEYBOARD_ROWS * cellH; keyboardStartY = screenH - keyboardHeight; if (keyboardStartY < 0) @@ -446,11 +447,8 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool if (textX < x) textX = x; // guard } else { - if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) { - textX = x + (width - textWidth + 1) / 2; - } else { - textX = x + (width - textWidth) / 2; - } + // Use ceil rounding for all screens (consistent with 128x64 behavior for numbers) + textX = x + (width - textWidth + 1) / 2; } int contentTop = y; int contentH = height; @@ -746,4 +744,4 @@ bool VirtualKeyboard::isTimedOut() const } } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index 66bbe1bfe8e..0ab647795d5 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -183,9 +183,13 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 static float segmentHeight = SEGMENT_HEIGHT * 0.75f; if (!scaleInitialized) { +#ifdef DISPLAY_FORCE_SMALL_FONTS + float screenwidth_target_ratio = 0.70f; // Target 70% of display width (adjustable) +#else float screenwidth_target_ratio = 0.80f; // Target 80% of display width (adjustable) - float max_scale = 3.5f; // Safety limit to avoid runaway scaling - float step = 0.05f; // Step increment per iteration +#endif + float max_scale = 3.5f; // Safety limit to avoid runaway scaling + float step = 0.05f; // Step increment per iteration float target_width = display->getWidth() * screenwidth_target_ratio; float target_height = diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp index 42600ce96e1..fe54d68e714 100644 --- a/src/graphics/draw/CompassRenderer.cpp +++ b/src/graphics/draw/CompassRenderer.cpp @@ -1,10 +1,6 @@ #include "configuration.h" #if HAS_SCREEN #include "CompassRenderer.h" -#include "NodeDB.h" -#include "UIRenderer.h" -#include "configuration.h" -#include "gps/GeoCoord.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include @@ -21,8 +17,8 @@ struct Point { void rotate(float angle) { - float cos_a = cos(angle); - float sin_a = sin(angle); + float cos_a = cosf(angle); + float sin_a = sinf(angle); float new_x = x * cos_a - y * sin_a; float new_y = x * sin_a + y * cos_a; x = new_x; @@ -51,21 +47,30 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, if (currentResolution == ScreenResolution::High) { radius += 4; } - Point north(0, -radius); - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) - north.rotate(-myHeading); - north.translate(compassX, compassY); + float northX = 0.0f; + float northY = -radius; + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) { + const float c = cosf(-myHeading); + const float s = sinf(-myHeading); + const float rx = northX * c - northY * s; + const float ry = northX * s + northY * c; + northX = rx; + northY = ry; + } + northX += compassX; + northY += compassY; display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); display->setColor(BLACK); + const int16_t nLabelWidth = display->getStringWidth("N"); if (currentResolution == ScreenResolution::High) { - display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6); + display->fillRect(northX - 8, northY - 1, nLabelWidth + 3, FONT_HEIGHT_SMALL - 6); } else { - display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6); + display->fillRect(northX - 4, northY - 1, nLabelWidth + 2, FONT_HEIGHT_SMALL - 6); } display->setColor(WHITE); - display->drawString(north.x, north.y - 3, "N"); + display->drawString(northX, northY - 3, "N"); } void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) @@ -113,11 +118,46 @@ void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, f display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y); } -float estimatedHeading(double lat, double lon) +bool getHeadingRadians(double lat, double lon, float &headingRadian) +{ + headingRadian = 0.0f; + + if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) + return true; + + if (!screen) + return false; + + if (screen->hasHeading()) { + headingRadian = screen->getHeading() * DEG_TO_RAD; + return true; + } + + const float estimatedHeadingDeg = screen->estimatedHeading(lat, lon); + if (!(estimatedHeadingDeg >= 0.0f)) + return false; + + headingRadian = estimatedHeadingDeg * DEG_TO_RAD; + return true; +} + +float adjustBearingForCompassMode(float bearingRadian, float headingRadian) { - // Simple magnetic declination estimation - // This is a very basic implementation - the original might be more sophisticated - return 0.0f; // Return 0 for now, indicating no heading available + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) + return bearingRadian - headingRadian; + + return bearingRadian; +} + +float radiansToDegrees360(float angleRadian) +{ + constexpr float fullTurnDeg = 360.0f; + float degrees = angleRadian * RAD_TO_DEG; + if (degrees < 0.0f) + degrees += fullTurnDeg; + else if (degrees >= fullTurnDeg) + degrees -= fullTurnDeg; + return degrees; } uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) @@ -137,4 +177,4 @@ uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) } // namespace CompassRenderer } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/draw/CompassRenderer.h b/src/graphics/draw/CompassRenderer.h index ca7532b6671..d7762384769 100644 --- a/src/graphics/draw/CompassRenderer.h +++ b/src/graphics/draw/CompassRenderer.h @@ -1,7 +1,6 @@ #pragma once #include "graphics/Screen.h" -#include "mesh/generated/meshtastic/mesh.pb.h" #include #include @@ -25,7 +24,9 @@ void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, u void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing); // Navigation and location functions -float estimatedHeading(double lat, double lon); +bool getHeadingRadians(double lat, double lon, float &headingRadian); +float adjustBearingForCompassMode(float bearingRadian, float headingRadian); +float radiansToDegrees360(float angleRadian); uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight); } // namespace CompassRenderer diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 24302c1db71..812d4576e07 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -1256,6 +1256,11 @@ void menuHandler::positionBaseMenu() if (accelerometerThread) { accelerometerThread->calibrate(30); } +#endif +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER + if (magnetometerThread) { + magnetometerThread->calibrate(30); + } #endif break; case PositionAction::GPSSmartPosition: diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index d7f0a1483a1..69f3d54d47b 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -373,14 +373,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } } - if (strlen(distStr) > 0) { - int offset = (currentResolution == ScreenResolution::High) - ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) - int rightEdge = x + columnWidth - offset; - int textWidth = display->getStringWidth(distStr); - display->drawString(rightEdge - textWidth, y, distStr); - } + const char *distanceLabel = (strlen(distStr) > 0) ? distStr : "?"; + int offset = (currentResolution == ScreenResolution::High) + ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) + int rightEdge = x + columnWidth - offset; + int textWidth = display->getStringWidth(distanceLabel); + display->drawString(rightEdge - textWidth, y, distanceLabel); } void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) @@ -431,8 +430,8 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } } -void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, - double userLat, double userLon) +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, + float myHeadingRadian, double userLat, double userLon) { if (!nodeDB->hasValidPosition(node)) return; @@ -446,11 +445,11 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 double nodeLat = node->position.latitude_i * 1e-7; double nodeLon = node->position.longitude_i * 1e-7; float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon); - float bearingToNode = RAD_TO_DEG * bearing; - float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); + float relativeBearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeadingRadian); + float relativeBearingDeg = CompassRenderer::radiansToDegrees360(relativeBearing); // Shrink size by 2px int size = FONT_HEIGHT_SMALL - 5; - CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing); + CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearingDeg); /* float angle = relativeBearing * DEG_TO_RAD; float halfSize = size / 2.0; @@ -480,12 +479,27 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 */ } +void drawCompassUnknown(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float, double, + double) +{ + if (!nodeDB->hasValidPosition(node)) + return; + + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); + int centerX = x + columnWidth - arrowXOffset; + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(centerX, y, "?"); +} + // ============================= // Main Screen Functions // ============================= void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, - EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon) + EntryRenderer renderer, NodeExtrasRenderer extras, float headingRadian, double lat, double lon) { const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; const int rowYOffset = FONT_HEIGHT_SMALL - 3; @@ -570,7 +584,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t renderer(display, node, xPos, yPos, columnWidth); if (extras) - extras(display, node, xPos, yPos, columnWidth, heading, lat, lon); + extras(display, node, xPos, yPos, columnWidth, headingRadian, lat, lon); lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL); yOffset += rowYOffset; @@ -765,9 +779,13 @@ void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t #endif void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - float heading = 0; - bool validHeading = false; + float headingRadian = 0.0f; auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (!ourNode || !nodeDB->hasValidPosition(ourNode)) { + drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, 0.0, 0.0); + return; + } + double lat = DegD(ourNode->position.latitude_i); double lon = DegD(ourNode->position.longitude_i); @@ -779,21 +797,12 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, lastSwitchTime = now; } #endif - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { -#if HAS_GPS - if (screen->hasHeading()) { - heading = screen->getHeading(); // degrees - validHeading = true; - } else { - heading = screen->estimatedHeading(lat, lon); - validHeading = !isnan(heading); - } -#endif - - if (!validHeading) - return; + if (!CompassRenderer::getHeadingRadians(lat, lon, headingRadian)) { + drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, lat, lon); + return; } - drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); + + drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, headingRadian, lat, lon); } /// Draw a series of fields in a column, wrapping to multiple columns if needed diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index be80a7d80bc..4aa21714111 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -32,7 +32,7 @@ enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATIO // Main node list screen function void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, - EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, + EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float headingRadian = 0, double lat = 0, double lon = 0); // Entry renderers @@ -43,8 +43,8 @@ void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); // Extras renderers -void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, - double userLat, double userLon); +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, + float myHeadingRadian, double userLat, double userLon); // Screen frame functions void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 92cc59a9ac6..17be46d5ce3 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -38,6 +38,15 @@ static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y) } } +static void drawCompassStatusText(OLEDDisplay *display, int16_t compassX, int16_t compassY, const char *statusLine1, + const char *statusLine2) +{ + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(compassX, compassY - FONT_HEIGHT_SMALL, statusLine1); + display->drawString(compassX, compassY, statusLine2); + display->setTextAlignment(TEXT_ALIGN_LEFT); +} + void graphics::UIRenderer::rebuildFavoritedNodes() { favoritedNodes.clear(); @@ -638,51 +647,54 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i display->drawString(x, getTextPositions(display)[line++], batLine); } + bool showCompass = false; + float myHeading = 0.0f; + float bearing = 0.0f; + const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode)); + const bool hasNodePositionFix = nodeDB->hasValidPosition(node); + const char *statusLine1 = nullptr; + const char *statusLine2 = nullptr; + if (hasOwnPositionFix && hasNodePositionFix) { + const auto &op = ourNode->position; + showCompass = CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading); + if (showCompass) { + const auto &p = node->position; + bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + bearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeading); + } else { + statusLine1 = "No"; + statusLine2 = "Heading"; + } + } else if (!hasOwnPositionFix || !hasNodePositionFix) { + statusLine1 = "No"; + statusLine2 = "Fix"; + } + // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- if (SCREEN_WIDTH > SCREEN_HEIGHT) { - bool showCompass = false; - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { - showCompass = true; - } - if (showCompass) { + if (showCompass || statusLine1) { const int16_t topY = getTextPositions(display)[1]; const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); const int16_t usableHeight = bottomY - topY - 5; int16_t compassRadius = usableHeight / 2; if (compassRadius < 8) compassRadius = 8; - const int16_t compassDiam = compassRadius * 2; const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + const int16_t compassDiam = compassRadius * 2; - const auto &op = ourNode->position; - float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 - : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - - const auto &p = node->position; - /* unused - float d = - GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - */ - float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { - myHeading = 0; + display->drawCircle(compassX, compassY, compassRadius); + if (showCompass) { + CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); } else { - bearing -= myHeading; + drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2); } - - display->drawCircle(compassX, compassY, compassRadius); - CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); } // else show nothing } else { // Portrait or square: put compass at the bottom and centered, scaled to fit available space - bool showCompass = false; - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { - showCompass = true; - } - if (showCompass) { + if (showCompass || statusLine1) { int yBelowContent = (line > 0 && line <= 5) ? (getTextPositions(display)[line - 1] + FONT_HEIGHT_SMALL + 2) : getTextPositions(display)[1]; const int margin = 4; @@ -693,8 +705,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i #else const int navBarHeight = 0; #endif - int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; // --------- END PATCH FOR EINK NAV BAR ----------- + int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; if (availableHeight < FONT_HEIGHT_SMALL * 2) return; @@ -708,25 +720,13 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i int compassX = x + SCREEN_WIDTH / 2; int compassY = yBelowContent + availableHeight / 2; - const auto &op = ourNode->position; - float myHeading = 0; - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { - myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 - : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - } - graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); - - const auto &p = node->position; - /* unused - float d = - GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - */ - float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) - bearing -= myHeading; - graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); - display->drawCircle(compassX, compassY, compassRadius); + if (showCompass) { + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); + } else { + drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2); + } } // else show nothing } @@ -1162,6 +1162,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU // === Header === graphics::drawCommonHeader(display, x, y, titleStr); + const int *textPos = getTextPositions(display); // === First Row: My Location === #if HAS_GPS @@ -1176,12 +1177,12 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - drawSatelliteIcon(display, x, getTextPositions(display)[line]); + drawSatelliteIcon(display, x, textPos[line]); int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; - display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); + display->drawString(x + 11 + xOffset, textPos[line++], displayLine); } else { // Onboard GPS - UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus); + UIRenderer::drawGps(display, 0, textPos[line++], gpsStatus); } config.display.heading_bold = origBold; @@ -1190,18 +1191,36 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), int32_t(gpsStatus->getAltitude())); - // === Determine Compass Heading === - float heading = 0; + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode)); + const bool hasLiveGpsFix = + (gpsStatus && gpsStatus->getHasLock() && (gpsStatus->getLatitude() != 0 || gpsStatus->getLongitude() != 0)); + const bool hasSensorHeading = screen->hasHeading(); + float heading = 0.0f; bool validHeading = false; - if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { - validHeading = true; - } else { - if (screen->hasHeading()) { - heading = radians(screen->getHeading()); - validHeading = true; + const char *statusLine1 = nullptr; + const char *statusLine2 = nullptr; + if (hasSensorHeading || hasLiveGpsFix || hasOwnPositionFix) { + double headingLat = 0.0; + double headingLon = 0.0; + if (hasLiveGpsFix) { + headingLat = DegD(gpsStatus->getLatitude()); + headingLon = DegD(gpsStatus->getLongitude()); + } else if (hasOwnPositionFix) { + const auto &op = ourNode->position; + headingLat = DegD(op.latitude_i); + headingLon = DegD(op.longitude_i); + } + validHeading = CompassRenderer::getHeadingRadians(headingLat, headingLon, heading); + } + + if (!validHeading) { + if (hasSensorHeading || hasLiveGpsFix || hasOwnPositionFix) { + statusLine1 = "No"; + statusLine2 = "Heading"; } else { - heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); - validHeading = !isnan(heading); + statusLine1 = "No"; + statusLine2 = "Fix"; } } @@ -1219,18 +1238,18 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), true); #endif - display->drawString(0, getTextPositions(display)[line++], uptimeStr); + display->drawString(0, textPos[line++], uptimeStr); } else { - display->drawString(0, getTextPositions(display)[line++], "Last: ?"); + display->drawString(0, textPos[line++], "Last: ?"); } // === Third Row: Line 1 GPS Info === - UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1"); + UIRenderer::drawGpsCoordinates(display, x, textPos[line++], gpsStatus, "line1"); if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC && uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { // === Fourth Row: Line 2 GPS Info === - UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2"); + UIRenderer::drawGpsCoordinates(display, x, textPos[line++], gpsStatus, "line2"); } // === Final Row: Altitude === @@ -1241,14 +1260,14 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU } else { snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt); } - display->drawString(x, getTextPositions(display)[line++], altitudeLine); + display->drawString(x, textPos[line++], altitudeLine); } #if !defined(OLED_TINY) - // === Draw Compass if heading is valid === - if (validHeading) { + // === Draw Compass === + if (validHeading || statusLine1) { // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- if (SCREEN_WIDTH > SCREEN_HEIGHT) { - const int16_t topY = getTextPositions(display)[1]; + const int16_t topY = textPos[1]; const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height const int16_t usableHeight = bottomY - topY - 5; @@ -1261,29 +1280,33 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU // Center vertically and nudge down slightly to keep "N" clear of header const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading); display->drawCircle(compassX, compassY, compassRadius); - - // "N" label - float northAngle = 0; - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) - northAngle = -heading; - float radius = compassRadius; - int16_t nX = compassX + (radius - 1) * sin(northAngle); - int16_t nY = compassY - (radius - 1) * cos(northAngle); - int16_t nLabelWidth = display->getStringWidth("N") + 2; - int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; - - display->setColor(BLACK); - display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); - display->setColor(WHITE); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + if (validHeading) { + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading); + + // "N" label + float northAngle = 0; + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) + northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } else { + drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2); + } } else { // Portrait or square: put compass at the bottom and centered, scaled to fit available space // For E-Ink screens, account for navigation bar at the bottom! - int yBelowContent = getTextPositions(display)[5] + FONT_HEIGHT_SMALL + 2; + int yBelowContent = textPos[5] + FONT_HEIGHT_SMALL + 2; const int margin = 4; int availableHeight = #if defined(USE_EINK) @@ -1304,25 +1327,29 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU int compassX = x + SCREEN_WIDTH / 2; int compassY = yBelowContent + availableHeight / 2; - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading); display->drawCircle(compassX, compassY, compassRadius); - - // "N" label - float northAngle = 0; - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) - northAngle = -heading; - float radius = compassRadius; - int16_t nX = compassX + (radius - 1) * sin(northAngle); - int16_t nY = compassY - (radius - 1) * cos(northAngle); - int16_t nLabelWidth = display->getStringWidth("N") + 2; - int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; - - display->setColor(BLACK); - display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); - display->setColor(WHITE); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + if (validHeading) { + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading); + + // "N" label + float northAngle = 0; + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) + northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } else { + drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2); + } } } #endif diff --git a/src/input/HapticFeedback.cpp b/src/input/HapticFeedback.cpp new file mode 100644 index 00000000000..fcd215be06d --- /dev/null +++ b/src/input/HapticFeedback.cpp @@ -0,0 +1,97 @@ +#include "HapticFeedback.h" + +#ifdef HAPTIC_FEEDBACK_PIN + +#include + +#ifdef HAPTIC_FEEDBACK_ACTIVE_LOW +#define HAPTIC_FEEDBACK_ON_STATE LOW +#define HAPTIC_FEEDBACK_OFF_STATE HIGH +#else +#define HAPTIC_FEEDBACK_ON_STATE HIGH +#define HAPTIC_FEEDBACK_OFF_STATE LOW +#endif + +HapticFeedback *hapticFeedback = nullptr; + +void initHapticFeedback() +{ + if (!hapticFeedback) + hapticFeedback = new HapticFeedback(); +} + +HapticFeedback::HapticFeedback() : concurrency::OSThread("Haptic") +{ + pinMode(HAPTIC_FEEDBACK_PIN, OUTPUT); + digitalWrite(HAPTIC_FEEDBACK_PIN, HAPTIC_FEEDBACK_OFF_STATE); +} + +void HapticFeedback::motorWrite(bool on) +{ + digitalWrite(HAPTIC_FEEDBACK_PIN, on ? HAPTIC_FEEDBACK_ON_STATE : HAPTIC_FEEDBACK_OFF_STATE); +} + +void HapticFeedback::pulse(uint16_t durationMs) +{ + motorWrite(true); + pulseOffAt = millis() + durationMs; + if (pulseOffAt == 0) // 0 is the "no pulse" sentinel + pulseOffAt = 1; + scheduleNext(); +} + +void HapticFeedback::armDelayedPulse(uint16_t delayMs, uint16_t durationMs) +{ + delayedPulseAt = millis() + delayMs; + if (delayedPulseAt == 0) + delayedPulseAt = 1; + delayedPulseDuration = durationMs; + scheduleNext(); +} + +void HapticFeedback::cancelDelayedPulse() +{ + delayedPulseAt = 0; +} + +void HapticFeedback::scheduleNext() +{ + uint32_t now = millis(); + uint32_t next = 0; + if (pulseOffAt != 0) + next = pulseOffAt; + if (delayedPulseAt != 0 && (next == 0 || (int32_t)(delayedPulseAt - next) < 0)) + next = delayedPulseAt; + if (next == 0) + return; + int32_t delay = (int32_t)(next - now); + setIntervalFromNow(delay > 0 ? (unsigned long)delay : 0); +} + +int32_t HapticFeedback::runOnce() +{ + uint32_t now = millis(); + + if (pulseOffAt != 0 && (int32_t)(now - pulseOffAt) >= 0) { + motorWrite(false); + pulseOffAt = 0; + } + + if (delayedPulseAt != 0 && (int32_t)(now - delayedPulseAt) >= 0) { + uint16_t dur = delayedPulseDuration; + delayedPulseAt = 0; + pulse(dur); + } + + uint32_t next = 0; + if (pulseOffAt != 0) + next = pulseOffAt; + if (delayedPulseAt != 0 && (next == 0 || (int32_t)(delayedPulseAt - next) < 0)) + next = delayedPulseAt; + if (next == 0) + return 60 * 1000; + int32_t delay = (int32_t)(next - now); + return delay > 0 ? delay : 0; +} + +#endif // HAPTIC_FEEDBACK_PIN diff --git a/src/input/HapticFeedback.h b/src/input/HapticFeedback.h new file mode 100644 index 00000000000..da542edeb2c --- /dev/null +++ b/src/input/HapticFeedback.h @@ -0,0 +1,35 @@ +#pragma once + +#include "configuration.h" + +#ifdef HAPTIC_FEEDBACK_PIN + +#include "concurrency/OSThread.h" +#include + +// Non-blocking pulses on a GPIO vibration motor. HAPTIC_FEEDBACK_ACTIVE_LOW inverts polarity. +class HapticFeedback : public concurrency::OSThread +{ + public: + HapticFeedback(); + void pulse(uint16_t durationMs = 30); + void armDelayedPulse(uint16_t delayMs, uint16_t durationMs = 30); + void cancelDelayedPulse(); + + protected: + int32_t runOnce() override; + + private: + uint32_t pulseOffAt = 0; + uint32_t delayedPulseAt = 0; + uint16_t delayedPulseDuration = 0; + + void motorWrite(bool on); + // Reschedule to the soonest pending event so later arms don't clobber earlier wakes. + void scheduleNext(); +}; + +extern HapticFeedback *hapticFeedback; +void initHapticFeedback(); + +#endif // HAPTIC_FEEDBACK_PIN diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index 42ab7f70d1e..c84eb2c0c0e 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -2,6 +2,7 @@ #include "PowerFSM.h" // needed for event trigger #include "configuration.h" #include "graphics/Screen.h" +#include "input/HapticFeedback.h" #include "modules/ExternalNotificationModule.h" #if ARCH_PORTDUINO @@ -237,6 +238,16 @@ void InputBroker::Init() } touchBacklightActive = false; }; +#endif +#if defined(HAPTIC_FEEDBACK_PIN) + // Blip on touch, second blip when long-press fires (500 ms = touchConfig.longPressTime default). + touchConfig.suppressLeadUpSound = true; + initHapticFeedback(); + touchConfig.onPress = []() { + hapticFeedback->pulse(80); + hapticFeedback->armDelayedPulse(500, 80); + }; + touchConfig.onRelease = []() { hapticFeedback->cancelDelayedPulse(); }; #endif TouchButtonThread->initButton(touchConfig); #endif diff --git a/src/main.cpp b/src/main.cpp index dab965c4c95..69448945ba9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -127,6 +127,10 @@ void printPartitionTable() #include "motion/AccelerometerThread.h" AccelerometerThread *accelerometerThread = nullptr; #endif +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER +#include "motion/MagnetometerThread.h" +MagnetometerThread *magnetometerThread = nullptr; +#endif #ifdef HAS_I2S #include "AudioThread.h" @@ -138,6 +142,10 @@ AudioThread *audioThread = nullptr; ExtensionIOXL9555 io; #endif +#ifdef USE_MCP23017 +#include "platform/esp32/ExtensionIOMCP23017.h" +#endif + #if HAS_TFT extern void tftSetup(void); #endif @@ -197,6 +205,8 @@ bool osk_found = false; ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE; // The I2C address of the Accelerometer (if found) ScanI2C::DeviceAddress accelerometer_found = ScanI2C::ADDRESS_NONE; +// The I2C address of the Magnetometer (if found) +ScanI2C::DeviceAddress magnetometer_found = ScanI2C::ADDRESS_NONE; // The I2C address of the RGB LED (if found) ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, ScanI2C::ADDRESS_NONE); /// The I2C address of our Air Quality Indicator (if found) @@ -422,6 +432,11 @@ void setup() digitalWrite(VEXT_ENABLE, VEXT_ON_VALUE); // turn on the display power #endif +#if defined(PIN_SENSOR_EN) + pinMode(PIN_SENSOR_EN, OUTPUT); + digitalWrite(PIN_SENSOR_EN, PIN_SENSOR_EN_ACTIVE); // turn on sensor power +#endif + #if defined(BIAS_T_ENABLE) pinMode(BIAS_T_ENABLE, OUTPUT); digitalWrite(BIAS_T_ENABLE, BIAS_T_VALUE); // turn on 5V for GPS Antenna @@ -519,6 +534,12 @@ void setup() powerStatus->observe(&power->newStatus); power->setup(); // Must be after status handler is installed, so that handler gets notified of the initial configuration +#ifdef USE_MCP23017 + // Bring up the I2C IO expander (LoRa reset, LCD reset, GPS wake) now that the PMU rails are up, + // before the I2C scan and radio/display init + mcp23017EarlyInit(); +#endif + #if !MESHTASTIC_EXCLUDE_I2C // We need to scan here to decide if we have a screen for nodeDB.init() and because power has been applied to // accessories @@ -662,6 +683,11 @@ void setup() accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found; LOG_DEBUG("acc_info = %i", acc_info.type); #endif +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_MAGNETOMETER + auto mag_info = i2cScanner->firstMagnetometer(); + magnetometer_found = mag_info.type != ScanI2C::DeviceType::NONE ? mag_info.address : magnetometer_found; + LOG_DEBUG("mag_info = %i", mag_info.type); +#endif scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA260, meshtastic_TelemetrySensorType_INA260); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA226, meshtastic_TelemetrySensorType_INA226); @@ -674,6 +700,8 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MMC5983MA, meshtastic_TelemetrySensorType_MMC5983MA); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM42607P, meshtastic_TelemetrySensorType_ICM42607P); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102); @@ -754,6 +782,11 @@ void setup() accelerometerThread = new AccelerometerThread(acc_info.type); } #endif +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_MAGNETOMETER + if (mag_info.type != ScanI2C::DeviceType::NONE) { + magnetometerThread = new MagnetometerThread(mag_info.type); + } +#endif #if defined(HAS_NEOPIXEL) || defined(UNPHONE) || defined(RGBLED_RED) ambientLightingThread = new AmbientLightingThread(ScanI2C::DeviceType::NONE); @@ -771,6 +804,17 @@ void setup() delay(10); #endif drv.begin(); + + // Bits Field Value Meaning + // 7 N_ERM_LRA 1 LRA mode (vs 0 = ERM) + // 6:4 FB_BRAKE_FACTOR 3 4× brake factor + // 3:2 LOOP_GAIN 1 medium loop gain + // 1:0 BEMF_GAIN 2 back-EMF gain + +#if defined(DRV2605_USE_LRA) + drv.writeRegister8(DRV2605_REG_FEEDBACK, 0xB6); +#endif + drv.selectLibrary(1); // I2C trigger by sending 'go' command drv.setMode(DRV2605_MODE_INTTRIG); diff --git a/src/main.h b/src/main.h index 56f048134cb..8d1b78258cc 100644 --- a/src/main.h +++ b/src/main.h @@ -35,6 +35,7 @@ extern bool kb_found; extern bool osk_found; extern ScanI2C::DeviceAddress rtc_found; extern ScanI2C::DeviceAddress accelerometer_found; +extern ScanI2C::DeviceAddress magnetometer_found; extern ScanI2C::FoundDevice rgb_found; extern ScanI2C::DeviceAddress aqi_found; @@ -69,6 +70,10 @@ extern graphics::Screen *screen; #include "motion/AccelerometerThread.h" extern AccelerometerThread *accelerometerThread; #endif +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER +#include "motion/MagnetometerThread.h" +extern MagnetometerThread *magnetometerThread; +#endif extern bool isVibrating; diff --git a/src/mesh/InterfacesTemplates.cpp b/src/mesh/InterfacesTemplates.cpp index c246141dcef..6e27b56f394 100644 --- a/src/mesh/InterfacesTemplates.cpp +++ b/src/mesh/InterfacesTemplates.cpp @@ -1,11 +1,17 @@ +#include "configuration.h" + #include "LR11x0Interface.cpp" #include "LR11x0Interface.h" +#include "LR20x0Interface.cpp" +#include "LR20x0Interface.h" #include "SX126xInterface.cpp" #include "SX126xInterface.h" #include "SX128xInterface.cpp" #include "SX128xInterface.h" +#ifndef ARCH_PORTDUINO_WASM // TCP socket API server excluded in the browser/wasm build #include "api/ServerAPI.cpp" #include "api/ServerAPI.h" +#endif // We need this declaration for proper linking in derived classes #if RADIOLIB_EXCLUDE_SX126X != 1 @@ -21,6 +27,9 @@ template class LR11x0Interface; template class LR11x0Interface; template class LR11x0Interface; #endif +#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1 +template class LR20x0Interface; +#endif #ifdef ARCH_STM32WL template class SX126xInterface; #endif diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index 4fec06da4b1..ec876239365 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -194,7 +194,7 @@ template bool LR11x0Interface::reconfigure() startReceive(); // restart receiving - return RADIOLIB_ERR_NONE; + return true; } template void LR11x0Interface::disableInterrupt() @@ -350,4 +350,10 @@ template bool LR11x0Interface::sleep() return true; } + +template int16_t LR11x0Interface::getCurrentRSSI() +{ + float rssi = lora.getRSSI(); + return (int16_t)round(rssi); +} #endif diff --git a/src/mesh/LR11x0Interface.h b/src/mesh/LR11x0Interface.h index 1a6b925206b..ee8761177da 100644 --- a/src/mesh/LR11x0Interface.h +++ b/src/mesh/LR11x0Interface.h @@ -37,6 +37,8 @@ template class LR11x0Interface : public RadioLibInterface */ T lora; + int16_t getCurrentRSSI() override; + /** * Glue functions called from ISR land */ diff --git a/src/mesh/LR2021Interface.cpp b/src/mesh/LR2021Interface.cpp new file mode 100644 index 00000000000..9aa4d5f1ab0 --- /dev/null +++ b/src/mesh/LR2021Interface.cpp @@ -0,0 +1,18 @@ +#include "configuration.h" + +#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1 + +#include "LR2021Interface.h" +#include "error.h" + +LR2021Interface::LR2021Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) + : LR20x0Interface(hal, cs, irq, rst, busy) +{ +} + +bool LR2021Interface::wideLora() +{ + return true; +} +#endif diff --git a/src/mesh/LR2021Interface.h b/src/mesh/LR2021Interface.h new file mode 100644 index 00000000000..52c04ee9034 --- /dev/null +++ b/src/mesh/LR2021Interface.h @@ -0,0 +1,15 @@ +#pragma once +#if RADIOLIB_EXCLUDE_LR2021 != 1 +#include "LR20x0Interface.h" + +/** + * Our adapter for LR2021 radios + */ +class LR2021Interface : public LR20x0Interface +{ + public: + LR2021Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy); + bool wideLora() override; +}; +#endif diff --git a/src/mesh/LR20x0Interface.cpp b/src/mesh/LR20x0Interface.cpp new file mode 100644 index 00000000000..95dad82e929 --- /dev/null +++ b/src/mesh/LR20x0Interface.cpp @@ -0,0 +1,383 @@ +#include "configuration.h" + +#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1 +#include "LR20x0Interface.h" +#include "error.h" +#include "mesh/NodeDB.h" + +// Keep LR20x0 naming while RadioLib exposes LR2021 symbols. +#ifndef LR20x0 +#define LR20x0 LR2021 +#endif + +#ifdef LR2021_DIO_AS_RF_SWITCH +#include "rfswitch.h" +#elif ARCH_PORTDUINO +#include "PortduinoGlue.h" +#define lr20x0_rfswitch_dio_pins portduino_config.rfswitch_dio_pins +#define lr20x0_rfswitch_table portduino_config.rfswitch_table +#else +static const uint32_t lr20x0_rfswitch_dio_pins[] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; +static const Module::RfSwitchMode_t lr20x0_rfswitch_table[] = { + {LR20x0::MODE_STBY, {}}, {LR20x0::MODE_RX, {}}, {LR20x0::MODE_TX, {}}, + {LR20x0::MODE_RX_HF, {}}, {LR20x0::MODE_TX_HF, {}}, END_OF_MODE_TABLE, +}; +#endif + +// Particular boards might define a different max power based on what their hardware can do, default to max power output if not +// specified (may be dangerous if using external PA and LR20x0 power config forgotten) +#if ARCH_PORTDUINO +#define LR2021_MAX_POWER portduino_config.lr2021_max_power +#endif +#ifndef LR2021_MAX_POWER +#define LR2021_MAX_POWER 22 +#endif + +// the 2.4G part maxes at 12dBm + +#if ARCH_PORTDUINO +#define LR2021_MAX_POWER_HF portduino_config.lr2021_max_power_hf +#endif +#ifndef LR2021_MAX_POWER_HF +#define LR2021_MAX_POWER_HF 12 +#endif + +template +LR20x0Interface::LR20x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) + : RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module) +{ + LOG_WARN("LR20x0Interface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); +} + +/// Initialise the Driver transport hardware and software. +/// Make sure the Driver is properly configured before calling init(). +/// \return true if initialisation succeeded. +template bool LR20x0Interface::init() +{ +#ifdef LR2021_POWER_EN + pinMode(LR2021_POWER_EN, OUTPUT); + digitalWrite(LR2021_POWER_EN, HIGH); +#endif + +#if ARCH_PORTDUINO + float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; +// FIXME: correct logic to default to not using TCXO if no voltage is specified for LR20x0_DIO3_TCXO_VOLTAGE +#elif defined(LR2021_DIO3_TCXO_VOLTAGE) + float tcxoVoltage = LR2021_DIO3_TCXO_VOLTAGE; + LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", LR2021_DIO3_TCXO_VOLTAGE); + // (DIO3 is not free to be used as an IRQ) +#elif defined(TCXO_OPTIONAL) + float tcxoVoltage = 1.6f; // TCXO_OPTIONAL: try default 1.6 V first, fall back to XTAL on failure + LOG_DEBUG("TCXO_OPTIONAL: no LR2021_DIO3_TCXO_VOLTAGE defined, trying default TCXO Vref 1.6 V first"); +#else + float tcxoVoltage = + 0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per + // https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104 + // (DIO3 is free to be used as an IRQ) + LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage"); +#endif + + RadioLibInterface::init(); + +#ifdef LR2021_IRQ_DIO_NUM + lora.irqDioNum = LR2021_IRQ_DIO_NUM; + LOG_DEBUG("Set irqDioNum %d", lora.irqDioNum); +#elif defined(IRQ_DIO_NUM) + lora.irqDioNum = IRQ_DIO_NUM; + LOG_DEBUG("Set irqDioNum %d", lora.irqDioNum); +#else + LOG_DEBUG("Use default irqDioNum %d", lora.irqDioNum); +#endif + + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range + limitPower(LR2021_MAX_POWER_HF); + } else { + limitPower(LR2021_MAX_POWER); // default clamp for non-wide freq range + } + +#ifdef LR2021_RF_SWITCH_SUBGHZ + pinMode(LR2021_RF_SWITCH_SUBGHZ, OUTPUT); + digitalWrite(LR2021_RF_SWITCH_SUBGHZ, getFreq() < 1e9 ? HIGH : LOW); + LOG_DEBUG("Set RF0 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); +#endif + +#ifdef LR2021_RF_SWITCH_2_4GHZ + pinMode(LR2021_RF_SWITCH_2_4GHZ, OUTPUT); + digitalWrite(LR2021_RF_SWITCH_2_4GHZ, getFreq() < 1e9 ? LOW : HIGH); + LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); +#endif + + // Allow extra time for TCXO to stabilize after power-on + delay(10); + + int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + + // Retry if we get SPI command failed - some units need extra TCXO stabilization time + if (res == RADIOLIB_ERR_SPI_CMD_FAILED) { + LOG_WARN("LR20x0 init failed with %d (SPI_CMD_FAILED), retrying after delay...", res); + delay(100); + res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + } + +#if defined(TCXO_OPTIONAL) + // If init failed for any reason other than chip not found, retry without TCXO (XTAL mode) + if (res != RADIOLIB_ERR_NONE && res != RADIOLIB_ERR_CHIP_NOT_FOUND && tcxoVoltage > 0) { + LOG_WARN("LR20x0 init failed with TCXO Vref %f V (err %d), retrying without TCXO", tcxoVoltage, res); + tcxoVoltage = 0; + res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + if (res == RADIOLIB_ERR_NONE) + LOG_INFO("LR20x0 init success without TCXO (XTAL mode)"); + } +#endif + + // \todo Display actual typename of the adapter, not just `LR20x0` + LOG_INFO("LR20x0 init result %d", res); + if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) + return false; + + LOG_INFO("Frequency set to %f", getFreq()); + LOG_INFO("Bandwidth set to %f", bw); + LOG_INFO("Power output set to %d", power); + + if (res == RADIOLIB_ERR_NONE) + res = lora.setCRC(2); + +#ifdef LR2021_DIO_AS_RF_SWITCH + bool dioAsRfSwitch = true; +#elif defined(ARCH_PORTDUINO) + bool dioAsRfSwitch = portduino_config.has_rfswitch_table; +#else + bool dioAsRfSwitch = false; +#endif + + if (dioAsRfSwitch) { + lora.setRfSwitchTable(lr20x0_rfswitch_dio_pins, lr20x0_rfswitch_table); + LOG_DEBUG("Set DIO RF switch"); + } + + if (res == RADIOLIB_ERR_NONE) { + if (config.lora.sx126x_rx_boosted_gain) { // the name is unfortunate but historically accurate + res = lora.setRxBoostedGainMode(true); + LOG_INFO("Set RX gain to boosted mode; result: %d", res); + } else { + res = lora.setRxBoostedGainMode(false); + LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", res); + } + } + + if (res == RADIOLIB_ERR_NONE) + startReceive(); // start receiving + + return res == RADIOLIB_ERR_NONE; +} + +template bool LR20x0Interface::reconfigure() +{ + RadioLibInterface::reconfigure(); + + // set mode to standby + setStandby(); + + // configure publicly accessible settings + int err = lora.setSpreadingFactor(sf); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + + err = lora.setBandwidth(bw); // different form than LR11xx + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + + err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + + err = lora.setSyncWord(syncWord); + assert(err == RADIOLIB_ERR_NONE); + + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range + limitPower(LR2021_MAX_POWER_HF); + } else { + limitPower(LR2021_MAX_POWER); // default clamp for non-wide freq range + } + + err = lora.setPreambleLength(preambleLength); + assert(err == RADIOLIB_ERR_NONE); + + err = lora.setFrequency(getFreq()); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + + err = lora.setOutputPower(power); + assert(err == RADIOLIB_ERR_NONE); + + // Apply RX gain mode - valid in STDBY, matches resetAGC() pattern + err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + if (err != RADIOLIB_ERR_NONE) + LOG_WARN("LR20x0 setRxBoostedGainMode %s%d", radioLibErr, err); + + startReceive(); // restart receiving + + return true; +} + +template void LR20x0Interface::disableInterrupt() +{ + lora.clearIrqAction(); +} + +template void LR20x0Interface::setStandby() +{ + checkNotification(); // handle any pending interrupts before we force standby + + int err = lora.standby(); + + if (err != RADIOLIB_ERR_NONE) { + LOG_DEBUG("LR20x0 standby failed with error %d", err); + } + + assert(err == RADIOLIB_ERR_NONE); + + isReceiving = false; // If we were receiving, not any more + activeReceiveStart = 0; + disableInterrupt(); + completeSending(); // If we were sending, not anymore + RadioLibInterface::setStandby(); +} + +/** + * Add SNR data to received messages + */ +template void LR20x0Interface::addReceiveMetadata(meshtastic_MeshPacket *mp) +{ + // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); + mp->rx_snr = lora.getSNR(); + mp->rx_rssi = lround(lora.getRSSI()); + // LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); // not implemented for LR20x0, but noop for LR11x0 + // too(!) +} + +/** We override to turn on transmitter power as needed. + */ +template void LR20x0Interface::configHardwareForSend() +{ + RadioLibInterface::configHardwareForSend(); +} + +// For power draw measurements, helpful to force radio to stay sleeping +// #define SLEEP_ONLY + +template void LR20x0Interface::startReceive() +{ +#ifdef SLEEP_ONLY + sleep(); +#else + + setStandby(); + + lora.setPreambleLength(preambleLength); // Solve RX ack fail after direct message sent. Not sure why this is needed. + + // We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly. + int err = + lora.startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS, RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + if (err) + LOG_ERROR("StartReceive error: %d", err); + assert(err == RADIOLIB_ERR_NONE); + + RadioLibInterface::startReceive(); + + // Must be done AFTER starting receive, because startReceive clears (possibly stale) interrupt pending register bits + enableInterrupt(isrRxLevel0); + checkRxDoneIrqFlag(); +#endif +} + +/** Is the channel currently active? */ +template bool LR20x0Interface::isChannelActive() +{ + // check if we can detect a LoRa preamble on the current channel + ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD, + .detPeak = RADIOLIB_LR2021_CAD_PARAM_DEFAULT, + .detMin = RADIOLIB_LR2021_CAD_PARAM_DEFAULT, + .exitMode = RADIOLIB_LR2021_CAD_PARAM_DEFAULT, + .timeout = 0, + .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, + .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; + int16_t result; + + setStandby(); + result = lora.scanChannel(cfg); + if (result == RADIOLIB_LORA_DETECTED) + return true; + + assert(result != RADIOLIB_ERR_WRONG_MODEM); + + return false; +} + +/** Could we send right now (i.e. either not actively receiving or transmitting)? */ +template bool LR20x0Interface::isActivelyReceiving() +{ + // The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet + // received and handled the interrupt for reading the packet/handling errors. + return receiveDetected(lora.getIrqStatus(), RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID, RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED); +} + +#ifdef LR20X0_AGC_RESET +template void LR20x0Interface::resetAGC() +{ + // Safety: don't reset mid-packet + if (sendingPacket != NULL || (isReceiving && isActivelyReceiving())) + return; + + LOG_DEBUG("LR20x0 AGC reset: warm sleep + Calibrate(0x3F)"); + + // 1. Warm sleep - powers down the analog frontend, resetting AGC state + lora.sleep(true, 0); + + // 2. Wake to RC standby for stable calibration + lora.standby(RADIOLIB_LR20X0_STANDBY_RC, true); + + // 3. Calibrate all blocks (PLL, ADC, image, RC oscillators) + // calibrate() is protected on LR20x0, so use raw SPI (same as internal implementation) + uint8_t calData = RADIOLIB_LR20X0_CALIBRATE_ALL; + module.SPIwriteStream(RADIOLIB_LR20X0_CMD_CALIBRATE, &calData, 1, true, true); + + // 4. Re-calibrate image rejection for actual operating frequency + // Calibrate(0x3F) defaults to 902-928 MHz which is wrong for other regions. + lora.calibrateImageRejection(getFreq() - 4.0f, getFreq() + 4.0f); + + // 5. Re-apply RX boosted gain mode + lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + + // 6. Resume receiving + startReceive(); +} +#endif + +template bool LR20x0Interface::sleep() +{ + // \todo Display actual typename of the adapter, not just `LR20x0` + LOG_DEBUG("LR20x0 entering sleep mode"); + setStandby(); // Stop any pending operations + + // turn off TCXO if it was powered + lora.setTCXO(0); + + // put chipset into sleep mode (we've already disabled interrupts by now) + bool keepConfig = false; + lora.sleep(keepConfig, 0); // Note: we do not keep the config, full reinit will be needed + +#ifdef LR2021_POWER_EN + digitalWrite(LR2021_POWER_EN, LOW); +#endif + + return true; +} + +template int16_t LR20x0Interface::getCurrentRSSI() +{ + float rssi = lora.getRSSI(false, true); + return (int16_t)round(rssi); +} +#endif diff --git a/src/mesh/LR20x0Interface.h b/src/mesh/LR20x0Interface.h new file mode 100644 index 00000000000..263c83429d2 --- /dev/null +++ b/src/mesh/LR20x0Interface.h @@ -0,0 +1,77 @@ +#pragma once +#if RADIOLIB_EXCLUDE_LR2021 != 1 +#include "RadioLibInterface.h" + +/** + * \brief Adapter for LR20x0 radio family. Implements common logic for child classes. + * \tparam T RadioLib module type for LR20x0, e.g. LR2021. + */ +template class LR20x0Interface : public RadioLibInterface +{ + public: + LR20x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy); + + /// Initialise the Driver transport hardware and software. + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool init() override; + + /// Apply any radio provisioning changes + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool reconfigure() override; + + /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. + virtual bool sleep() override; + + bool isIRQPending() override { return lora.getIrqFlags() != 0; } + +#ifdef LR20X0_AGC_RESET + void resetAGC() override; +#endif + + protected: + /** + * Specific module instance + */ + T lora; + + int16_t getCurrentRSSI() override; + + /** + * Glue functions called from ISR land + */ + virtual void disableInterrupt() override; + + /** + * Enable a particular ISR callback glue function + */ + virtual void enableInterrupt(void (*callback)()) { lora.setIrqAction(callback); } + + /** can we detect a LoRa preamble on the current channel? */ + virtual bool isChannelActive() override; + + /** are we actively receiving a packet (only called during receiving state) */ + virtual bool isActivelyReceiving() override; + + /** + * Start waiting to receive a message + */ + virtual void startReceive() override; + + /** + * We override to turn on transmitter power as needed. + */ + virtual void configHardwareForSend() override; + + /** + * Add SNR data to received messages + */ + virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; + + virtual void setStandby() override; + + uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } +}; +#endif diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 3ce78513cb7..22a2e1f364a 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -7,8 +7,12 @@ #include "CryptoEngine.h" #include "Default.h" #include "FSCommon.h" +#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) +#include +#endif #include "MeshRadio.h" #include "MeshService.h" +#include "MessageStore.h" #include "NodeDB.h" #include "PacketHistory.h" #include "PowerFSM.h" @@ -524,6 +528,9 @@ bool NodeDB::factoryReset(bool eraseBleBonds) if (transmitHistory) { transmitHistory->clear(); } +#if HAS_SCREEN + messageStore.clearAllMessages(); +#endif // second, install default state (this will deal with the duplicate mac address issue) installDefaultNodeDatabase(); installDefaultDeviceState(); @@ -2172,23 +2179,52 @@ bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_pub } #endif +#ifdef FSCom +// Shared by the FLASH and SD backup locations so the two paths can't drift apart +static meshtastic_BackupPreferences buildBackupPreferences() +{ + meshtastic_BackupPreferences backup = meshtastic_BackupPreferences_init_zero; + backup.version = DEVICESTATE_CUR_VER; + backup.timestamp = getValidTime(RTCQuality::RTCQualityDevice, false); + backup.has_config = true; + backup.config = config; + backup.has_module_config = true; + backup.module_config = moduleConfig; + backup.has_channels = true; + backup.channels = channelFile; + backup.has_owner = true; + backup.owner = owner; + return backup; +} + +static void applyRestoredPreferences(const meshtastic_BackupPreferences &backup, int restoreWhat) +{ + if (restoreWhat & SEGMENT_CONFIG) { + config = backup.config; + LOG_DEBUG("Restored config"); + } + if (restoreWhat & SEGMENT_MODULECONFIG) { + moduleConfig = backup.module_config; + LOG_DEBUG("Restored module config"); + } + if (restoreWhat & SEGMENT_DEVICESTATE) { + devicestate.owner = backup.owner; + LOG_DEBUG("Restored device state"); + } + if (restoreWhat & SEGMENT_CHANNELS) { + channelFile = backup.channels; + LOG_DEBUG("Restored channels"); + } +} +#endif + bool NodeDB::backupPreferences(meshtastic_AdminMessage_BackupLocation location) { bool success = false; lastBackupAttempt = millis(); #ifdef FSCom if (location == meshtastic_AdminMessage_BackupLocation_FLASH) { - meshtastic_BackupPreferences backup = meshtastic_BackupPreferences_init_zero; - backup.version = DEVICESTATE_CUR_VER; - backup.timestamp = getValidTime(RTCQuality::RTCQualityDevice, false); - backup.has_config = true; - backup.config = config; - backup.has_module_config = true; - backup.module_config = moduleConfig; - backup.has_channels = true; - backup.channels = channelFile; - backup.has_owner = true; - backup.owner = owner; + meshtastic_BackupPreferences backup = buildBackupPreferences(); size_t backupSize; pb_get_encoded_size(&backupSize, meshtastic_BackupPreferences_fields, &backup); @@ -2204,7 +2240,33 @@ bool NodeDB::backupPreferences(meshtastic_AdminMessage_BackupLocation location) LOG_ERROR("Failed to save backup preferences to file"); } } else if (location == meshtastic_AdminMessage_BackupLocation_SD) { - // TODO: After more mainline SD card support +#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) + meshtastic_BackupPreferences backup = buildBackupPreferences(); + + std::vector buffer(meshtastic_BackupPreferences_size); + pb_ostream_t stream = pb_ostream_from_buffer(buffer.data(), buffer.size()); + if (!pb_encode(&stream, &meshtastic_BackupPreferences_msg, &backup)) { + LOG_ERROR("Failed to encode backup preferences"); + return false; + } + + concurrency::LockGuard g(spiLock); + SD.mkdir("/backups"); + File file = SD.open(backupFileName, FILE_WRITE); + if (!file) { + LOG_ERROR("Failed to open %s on SD card (no card inserted?)", backupFileName); + return false; + } + success = file.write(buffer.data(), stream.bytes_written) == stream.bytes_written; + file.close(); + if (success) { + LOG_INFO("Saved backup preferences to SD card"); + } else { + LOG_ERROR("Failed to save backup preferences to SD card"); + } +#else + LOG_ERROR("SD card backup requested, but this device has no SD card support"); +#endif } #endif return success; @@ -2227,22 +2289,7 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location, success = loadProto(backupFileName, meshtastic_BackupPreferences_size, sizeof(meshtastic_BackupPreferences), &meshtastic_BackupPreferences_msg, &backup); if (success) { - if (restoreWhat & SEGMENT_CONFIG) { - config = backup.config; - LOG_DEBUG("Restored config"); - } - if (restoreWhat & SEGMENT_MODULECONFIG) { - moduleConfig = backup.module_config; - LOG_DEBUG("Restored module config"); - } - if (restoreWhat & SEGMENT_DEVICESTATE) { - devicestate.owner = backup.owner; - LOG_DEBUG("Restored device state"); - } - if (restoreWhat & SEGMENT_CHANNELS) { - channelFile = backup.channels; - LOG_DEBUG("Restored channels"); - } + applyRestoredPreferences(backup, restoreWhat); success = saveToDisk(restoreWhat); if (success) { @@ -2254,7 +2301,46 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location, LOG_ERROR("Failed to restore preferences from backup file"); } } else if (location == meshtastic_AdminMessage_BackupLocation_SD) { - // TODO: After more mainline SD card support +#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) + std::vector buffer; + { + concurrency::LockGuard g(spiLock); + File file = SD.open(backupFileName, FILE_READ); + if (!file) { + LOG_WARN("Could not restore. No backup file found on SD card"); + return false; + } + size_t fileSize = file.size(); + if (fileSize == 0 || fileSize > meshtastic_BackupPreferences_size + 256) { + file.close(); + LOG_ERROR("Could not restore. Backup file on SD card has implausible size %u", (unsigned)fileSize); + return false; + } + buffer.resize(fileSize); + if ((size_t)file.read(buffer.data(), fileSize) != fileSize) { + file.close(); + LOG_ERROR("Could not restore. Failed to read backup file from SD card"); + return false; + } + file.close(); + } + meshtastic_BackupPreferences backup = meshtastic_BackupPreferences_init_zero; + pb_istream_t stream = pb_istream_from_buffer(buffer.data(), buffer.size()); + success = pb_decode(&stream, &meshtastic_BackupPreferences_msg, &backup); + if (!success) { + LOG_ERROR("Failed to decode backup preferences from SD card"); + return false; + } + applyRestoredPreferences(backup, restoreWhat); + success = saveToDisk(restoreWhat); + if (success) { + LOG_INFO("Restored preferences from SD card backup"); + } else { + LOG_ERROR("Failed to save restored preferences to flash"); + } +#else + LOG_ERROR("SD card restore requested, but this device has no SD card support"); +#endif } #endif return success; diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 9ff27d33eaa..6c2639cb1f2 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -36,6 +36,17 @@ // Flag to indicate a heartbeat was received and we should send queue status bool heartbeatReceived = false; +namespace +{ +constexpr uint8_t FILES_MANIFEST_LEVELS = 3; +constexpr size_t FILES_MANIFEST_MAX_COUNT = 64; + +void releaseFilesManifest(std::vector &filesManifest) +{ + std::vector().swap(filesManifest); +} +} // namespace + PhoneAPI::PhoneAPI() { lastContactMsec = millis(); @@ -70,10 +81,23 @@ void PhoneAPI::handleStartConfig() state = STATE_SEND_MY_INFO; } pauseBluetoothLogging = true; - spiLock->lock(); - filesManifest = getFiles("/", 10); - spiLock->unlock(); - LOG_DEBUG("Got %d files in manifest", filesManifest.size()); + // Manifest is never read on the node-info-only path (STATE_SEND_FILEMANIFEST + // short-circuits to sendConfigComplete), so skip the SPI lock + FS walk. + if (config_nonce != SPECIAL_NONCE_ONLY_NODES) { + bool filesManifestLimited = false; + { + concurrency::LockGuard guard(spiLock); + filesManifest = getFiles("/", FILES_MANIFEST_LEVELS, FILES_MANIFEST_MAX_COUNT, &filesManifestLimited); + } + if (filesManifestLimited) { + LOG_WARN("Got %zu files in manifest (limited to %zu entries/depth %u)", filesManifest.size(), + FILES_MANIFEST_MAX_COUNT, static_cast(FILES_MANIFEST_LEVELS)); + } else { + LOG_DEBUG("Got %zu files in manifest", filesManifest.size()); + } + } else { + releaseFilesManifest(filesManifest); + } LOG_INFO("Start API client config millis=%u", millis()); // Protect against concurrent BLE callbacks: they run in NimBLE's FreeRTOS task and also touch nodeInfoQueue. @@ -122,8 +146,7 @@ void PhoneAPI::close() nodeInfoQueue.clear(); } packetForPhone = NULL; - filesManifest.clear(); - filesManifest.shrink_to_fit(); + releaseFilesManifest(filesManifest); lastPortNumToRadio.clear(); fromRadioNum = 0; config_nonce = 0; @@ -532,7 +555,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) if (config_state == filesManifest.size() || config_nonce == SPECIAL_NONCE_ONLY_NODES) { // also handles an empty filesManifest config_state = 0; - filesManifest.clear(); + releaseFilesManifest(filesManifest); // Skip to complete packet sendConfigComplete(); } else { diff --git a/src/mesh/PositionPrecision.cpp b/src/mesh/PositionPrecision.cpp index 04db01c7919..75a17d6e9db 100644 --- a/src/mesh/PositionPrecision.cpp +++ b/src/mesh/PositionPrecision.cpp @@ -4,17 +4,19 @@ #include -uint32_t getPositionPrecisionForChannel(uint8_t channelIndex) +uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel) { - const meshtastic_Channel &channel = channels.getByIndex(channelIndex); - if (channel.settings.has_module_settings) { return channel.settings.module_settings.position_precision; - } else if (channel.role == meshtastic_Channel_Role_PRIMARY) { - return 32; - } else { - return 0; } + // No module settings: fail closed. A PRIMARY channel used to default to 32 + // here, leaking an exact position on a sharing-disabled channel. See #10509. + return 0; +} + +uint32_t getPositionPrecisionForChannel(uint8_t channelIndex) +{ + return getPositionPrecisionForChannel(channels.getByIndex(channelIndex)); } static int32_t truncateCoordinate(int32_t coordinate, uint32_t precision) diff --git a/src/mesh/PositionPrecision.h b/src/mesh/PositionPrecision.h index 6fdbd2f6435..89828f2e03f 100644 --- a/src/mesh/PositionPrecision.h +++ b/src/mesh/PositionPrecision.h @@ -1,8 +1,10 @@ #pragma once +#include "meshtastic/channel.pb.h" #include "meshtastic/mesh.pb.h" #include +uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel); uint32_t getPositionPrecisionForChannel(uint8_t channelIndex); void applyPositionPrecision(meshtastic_Position &position, uint32_t precision); bool applyPositionPrecision(meshtastic_MeshPacket &packet, uint32_t precision); diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index b3aa72f7ae7..db82aac79b3 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -253,7 +253,7 @@ bool RF95Interface::reconfigure() startReceive(); // restart receiving - return RADIOLIB_ERR_NONE; + return true; } /** @@ -342,4 +342,10 @@ bool RF95Interface::sleep() return true; } + +int16_t RF95Interface::getCurrentRSSI() +{ + float rssi = lora->getRSSI(false); + return (int16_t)round(rssi); +} #endif \ No newline at end of file diff --git a/src/mesh/RF95Interface.h b/src/mesh/RF95Interface.h index ffd8ae0082b..2226067646e 100644 --- a/src/mesh/RF95Interface.h +++ b/src/mesh/RF95Interface.h @@ -37,6 +37,8 @@ class RF95Interface : public RadioLibInterface */ virtual void disableInterrupt() override; + int16_t getCurrentRSSI() override; + /** * Enable a particular ISR callback glue function */ diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 6a8a0230a7d..d20be1e5726 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -5,6 +5,7 @@ #include "LR1110Interface.h" #include "LR1120Interface.h" #include "LR1121Interface.h" +#include "LR2021Interface.h" #include "MeshRadio.h" #include "MeshService.h" #include "NodeDB.h" @@ -28,10 +29,16 @@ #include "platform/portduino/USBHal.h" #endif +#if defined(ARCH_ESP32) && defined(USE_MCP23017) +#include "platform/esp32/MCP23017LockingArduinoHal.h" +#endif + #ifdef ARCH_STM32WL #include "STM32WLE5JCInterface.h" #endif +Observable RadioInterface::loraRxPacketObservable; + #define RDEF(name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, frequency_switching, wide_lora) \ { \ meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, \ @@ -294,6 +301,10 @@ std::unique_ptr initLoRa() #elif defined(HW_SPI1_DEVICE) LockingArduinoHal *loraHal = new LockingArduinoHal(SPI1, loraSpiSettings); RadioLibHAL = loraHal; +#elif defined(ARCH_ESP32) && defined(USE_MCP23017) + // Radio control lines (RESET/DIO1/BUSY) are virtual pins on an MCP23017 I2C expander + LockingArduinoHal *loraHal = new MCP23017LockingArduinoHal(SPI, loraSpiSettings, mcpIoExpander); + RadioLibHAL = loraHal; #else // HW_SPI1_DEVICE LockingArduinoHal *loraHal = new LockingArduinoHal(SPI, loraSpiSettings); RadioLibHAL = loraHal; @@ -458,6 +469,20 @@ std::unique_ptr initLoRa() } #endif +#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1 + if (!rIf) { + rIf = std::unique_ptr( + new LR2021Interface(loraHal, LR2021_SPI_NSS_PIN, LR2021_IRQ_PIN, LR2021_NRESET_PIN, LR2021_BUSY_PIN)); + if (!rIf->init()) { + LOG_WARN("No LR2021 radio"); + rIf = nullptr; + } else { + LOG_INFO("LR2021 init success"); + radioType = LR2021_RADIO; + } + } +#endif + #if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1 if (!rIf) { rIf = std::unique_ptr(new SX1280Interface(loraHal, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY)); diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 8f793f47ae2..a3eca415aba 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -95,8 +95,11 @@ class RadioInterface const uint8_t NUM_SYM_CAD = 2; // Number of symbols used for CAD, 2 is the default since RadioLib 6.3.0 as per AN1200.48 const uint8_t NUM_SYM_CAD_24GHZ = 4; // Number of symbols used for CAD in 2.4 GHz, 4 is recommended in AN1200.22 of SX1280 uint32_t slotTimeMsec = computeSlotTimeMsec(); - uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving - uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast + uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving + static constexpr uint16_t preambleLengthDefault = + 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving + static constexpr uint16_t wideLoraPreambleLengthDefault = 12; // 12 is default for wide Lora + uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast const uint32_t PROCESSING_TIME_MSEC = 4500; // time to construct, process and construct a packet again (empirically determined) const uint8_t CWmin = 3; // minimum CWsize @@ -123,6 +126,9 @@ class RadioInterface virtual ~RadioInterface() {} + /// Fires once per valid received LoRa packet (arg = sender NodeNum). Used e.g. to flash LED_LORA. + static Observable loraRxPacketObservable; + /** * Coerce LoRa config fields (bandwidth/spread_factor) derived from presets. * This is used during early bootstrapping so UIs that display these fields directly remain consistent. @@ -237,8 +243,8 @@ class RadioInterface protected: int8_t power = 17; // Set by applyModemConfig() - float savedFreq; - uint32_t savedChannelNum; + float savedFreq = 0.0f; + uint32_t savedChannelNum = 0; /*** * given a packet set sendingPacket and decode the protobufs into radiobuf. Returns # of bytes to send (including the @@ -264,6 +270,12 @@ class RadioInterface */ virtual void saveChannelNum(uint32_t savedChannelNum); + /** + * Get current RSSI reading from the radio. + * Returns 0 if not available. + */ + virtual int16_t getCurrentRSSI() { return 0; } + private: /** * Convert our modemConfig enum into wf, sf, etc... diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 5121ac4335a..b1db591b5af 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -15,6 +15,7 @@ #include "PortduinoGlue.h" #include "meshUtils.h" #endif + void LockingArduinoHal::spiBeginTransaction() { spiLock->lock(); @@ -28,6 +29,7 @@ void LockingArduinoHal::spiEndTransaction() spiLock->unlock(); } + #if ARCH_PORTDUINO void LockingArduinoHal::spiTransfer(uint8_t *out, size_t len, uint8_t *in) { @@ -40,12 +42,24 @@ RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE c : NotifiedWorkerThread("RadioIf"), module(hal, cs, irq, rst, busy), iface(_iface) { instance = this; + + // Initialize unused sample slots to a sane default; sample count controls averaging. + for (uint8_t i = 0; i < NOISE_FLOOR_SAMPLES; i++) { + noiseFloorSamples[i] = NOISE_FLOOR_DEFAULT; + } + #if defined(ARCH_STM32WL) && defined(USE_SX1262) module.setCb_digitalWrite(stm32wl_emulate_digitalWrite); module.setCb_digitalRead(stm32wl_emulate_digitalRead); #endif } +static bool radioFrequencyChanged(float previousFreq, float currentFreq) +{ + const float delta = currentFreq - previousFreq; + return delta > 0.000001f || delta < -0.000001f; +} + #ifdef ARCH_ESP32 // ESP32 doesn't use that flag #define YIELD_FROM_ISR(x) portYIELD_FROM_ISR() @@ -220,6 +234,16 @@ bool RadioLibInterface::canSleep() return res; } +bool RadioLibInterface::reconfigure() +{ + const float previousFreq = getFreq(); + bool result = RadioInterface::reconfigure(); + if (result && radioFrequencyChanged(previousFreq, getFreq())) { + resetNoiseFloor(); + } + return result; +} + /** Allow other firmware components to ask whether we are currently sending a packet Initially implemented to protect T-Echo's capacitive touch button from spurious presses during tx */ @@ -246,6 +270,88 @@ bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id) return txQueue.find(from, id); } +void RadioLibInterface::updateNoiseFloor() +{ + // Only sample from idle receive mode. TX/RX-critical paths must return to radio work quickly. + if (!isReceiving || sendingPacket != NULL || isActivelyReceiving() || isIRQPending()) { + return; + } + + uint32_t now = millis(); + if (now - lastNoiseFloorUpdate < NOISE_FLOOR_UPDATE_INTERVAL_MS) { + return; + } + lastNoiseFloorUpdate = now; + + int16_t rssi = getCurrentRSSI(); + if (rssi == NOISE_FLOOR_INVALID || rssi >= 0 || rssi < NOISE_FLOOR_VALID_MIN) { + LOG_DEBUG("Skipping invalid RSSI reading: %d", rssi); + return; + } + + noiseFloorSamples[currentSampleIndex] = (int32_t)rssi; + currentSampleIndex++; + + if (currentSampleIndex >= NOISE_FLOOR_SAMPLES) { + currentSampleIndex = 0; + isNoiseFloorBufferFull = true; + } + + currentNoiseFloor = getAverageNoiseFloorInternal(); + + LOG_DEBUG("Noise floor: %d dBm (samples: %d, latest: %d dBm)", currentNoiseFloor, getNoiseFloorSampleCountInternal(), rssi); +} + +uint8_t RadioLibInterface::getNoiseFloorSampleCountInternal() const +{ + return isNoiseFloorBufferFull ? NOISE_FLOOR_SAMPLES : currentSampleIndex; +} + +int32_t RadioLibInterface::getAverageNoiseFloorInternal() const +{ + uint8_t sampleCount = getNoiseFloorSampleCountInternal(); + + if (sampleCount == 0) { + return NOISE_FLOOR_DEFAULT; + } + + int32_t sum = 0; + for (uint8_t i = 0; i < sampleCount; i++) { + sum += noiseFloorSamples[i]; + } + + return sum / sampleCount; +} + +int32_t RadioLibInterface::getAverageNoiseFloor() +{ + return getAverageNoiseFloorInternal(); +} + +int32_t RadioLibInterface::getNoiseFloor() +{ + return currentNoiseFloor; +} + +bool RadioLibInterface::hasNoiseFloorSamples() +{ + return getNoiseFloorSampleCountInternal() > 0; +} + +uint8_t RadioLibInterface::getNoiseFloorSampleCount() +{ + return getNoiseFloorSampleCountInternal(); +} + +void RadioLibInterface::resetNoiseFloor() +{ + currentSampleIndex = 0; + isNoiseFloorBufferFull = false; + lastNoiseFloorUpdate = 0; + currentNoiseFloor = NOISE_FLOOR_DEFAULT; + LOG_INFO("Noise floor reset - rolling window collection will restart"); +} + bool RadioLibInterface::randomBytes(uint8_t *buffer, size_t length) { if (!buffer || length == 0 || !iface) { @@ -271,8 +377,43 @@ The CW size is determined by setTransmitDelay() and depends either on the curren of a flooding message. After this, we perform channel activity detection (CAD) and reset the transmit delay if it is currently active. */ +// In software-IRQ-poll mode (LORA_DIO1_SOFTWARE_POLL) a 1ms poll tick is almost always pending, so +// TX timers must be allowed to overwrite the pending notification or TX scheduling starves. On all +// other targets keep the historical non-overwriting behavior. +#ifdef LORA_DIO1_SOFTWARE_POLL +static constexpr bool txTimerOverwrite = true; +#else +static constexpr bool txTimerOverwrite = false; +#endif + +// cppcheck-suppress constParameterPointer ; a function pointer can't meaningfully point to const +bool RadioLibInterface::isIsrTxCallback(void (*callback)()) +{ + return callback == isrTxLevel0; +} + +void RadioLibInterface::scheduleIrqPollTick() +{ + // Never overwrite a pending notification (especially TRANSMIT_DELAY_COMPLETED), + // otherwise poll ticks would starve TX scheduling. + // + // There is a single notification slot, so while a TX is queued and the radio is busy receiving, + // the self-rescheduling TRANSMIT_DELAY_COMPLETED timer (which does overwrite, see txTimerOverwrite) + // can keep the slot and prevent a poll tick from being scheduled. In that window a completing + // RX/TX is not seen by the poll; RadioInterface's pollMissedIrqs() (~1s) is the backup that + // recovers it, so the effect is bounded added latency under heavy contention, not a lost event. + notifyLater(1, ISR_POLL_TICK, false); +} + +void RadioLibInterface::deliverPendingIrqFromPoll(PendingISR cause) +{ + disableInterrupt(); // stop polling; this is the poll-path equivalent of isrLevel0Common() + notify(cause, true); +} + void RadioLibInterface::onNotify(uint32_t notification) { + switch (notification) { case ISR_TX: handleTransmitInterrupt(); @@ -284,6 +425,9 @@ void RadioLibInterface::onNotify(uint32_t notification) startReceive(); setTransmitDelay(); break; + case ISR_POLL_TICK: + handleSoftwareLoraIrqPoll(); + break; case TRANSMIT_DELAY_COMPLETED: // If we are not currently in receive mode, then restart the random delay (this can happen if the main thread @@ -297,7 +441,7 @@ void RadioLibInterface::onNotify(uint32_t notification) long delay_remaining = txp->tx_after ? txp->tx_after - millis() : 0; if (delay_remaining > 0) { // There's still some delay pending on this packet, so resume waiting for it to elapse - notifyLater(delay_remaining, TRANSMIT_DELAY_COMPLETED, false); + notifyLater(delay_remaining, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite); } else { if (isChannelActive()) { // check if there is currently a LoRa packet on the channel startReceive(); // try receiving this packet, afterwards we'll be trying to transmit again @@ -336,7 +480,7 @@ void RadioLibInterface::setTransmitDelay() unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p) : getTxDelayMsec(); unsigned long now = millis(); p->tx_after = min(max(p->tx_after + add_delay, now + add_delay), now + 2 * getTxDelayMsecWeightedWorst(p->rx_snr)); - notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, false); + notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite); } else if (p->rx_snr == 0 && p->rx_rssi == 0) { /* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally. * This assumption is valid because of the offset generated by the radio to account for the noise @@ -355,7 +499,7 @@ void RadioLibInterface::startTransmitTimer(bool withDelay) // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { uint32_t delay = !withDelay ? 1 : getTxDelayMsec(); - notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable + notifyLater(delay, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite); // This will implicitly enable } } @@ -364,7 +508,7 @@ void RadioLibInterface::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { uint32_t delay = getTxDelayMsecWeighted(p); - notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable + notifyLater(delay, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite); // This will implicitly enable } } @@ -404,11 +548,6 @@ bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_ return false; } -/** - * Remove a packet that is eligible for replacement from the TX queue - */ -// void RadioLibInterface::removePending - void RadioLibInterface::handleTransmitInterrupt() { // This can be null if we forced the device to enter standby mode. In that case @@ -424,6 +563,9 @@ void RadioLibInterface::completeSending() // that can take a long time auto p = sendingPacket; sendingPacket = NULL; +#ifdef LED_LORA + digitalWrite(LED_LORA, LED_STATE_OFF); +#endif if (p) { // Packet has been sent, count it toward our TX airtime utilization. @@ -526,6 +668,10 @@ void RadioLibInterface::handleReceiveInterrupt() printPacket("Lora RX", mp); +#ifdef LED_LORA + loraRxPacketObservable.notifyObservers(mp->from); +#endif + airTime->logAirtime(RX_LOG, rxMsec); deliverToReceiver(mp); @@ -601,6 +747,9 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp) enableInterrupt(isrTxLevel0); lastTxStart = millis(); printPacket("Started Tx", txp); +#ifdef LED_LORA + digitalWrite(LED_LORA, LED_STATE_ON); +#endif } return res == RADIOLIB_ERR_NONE; diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 9ee60821414..b832b3badf4 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -52,17 +52,17 @@ class STM32WLx_ModuleWrapper : public STM32WLx_Module class RadioLibInterface : public RadioInterface, protected concurrency::NotifiedWorkerThread { + MeshPacketQueue txQueue = MeshPacketQueue(MAX_TX_QUEUE); + + protected: /// Used as our notification from the ISR - enum PendingISR { ISR_NONE = 0, ISR_RX, ISR_TX, TRANSMIT_DELAY_COMPLETED }; + enum PendingISR { ISR_NONE = 0, ISR_RX, ISR_TX, TRANSMIT_DELAY_COMPLETED, ISR_POLL_TICK }; /** * Raw ISR handler that just calls our polymorphic method */ static void isrTxLevel0(), isrLevel0Common(PendingISR code); - MeshPacketQueue txQueue = MeshPacketQueue(MAX_TX_QUEUE); - - protected: ModemType_t modemType = RADIOLIB_MODEM_LORA; DataRate_t getDataRate() const { return {.lora = {.spreadingFactor = sf, .bandwidth = bw, .codingRate = cr}}; } PacketConfig_t getPacketConfig() const @@ -99,11 +99,42 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified /// are _trying_ to receive a packet currently (note - we might just be waiting for one) bool isReceiving = false; + protected: + // Noise floor tracking - rolling window of samples. + static const uint8_t NOISE_FLOOR_SAMPLES = 20; + static const int32_t NOISE_FLOOR_DEFAULT = -120; + static const int32_t NOISE_FLOOR_VALID_MIN = -127; + static const int32_t NOISE_FLOOR_INVALID = -128; + int32_t noiseFloorSamples[NOISE_FLOOR_SAMPLES]; + uint8_t currentSampleIndex = 0; + bool isNoiseFloorBufferFull = false; + uint32_t lastNoiseFloorUpdate = 0; + static const uint32_t NOISE_FLOOR_UPDATE_INTERVAL_MS = 5000; + int32_t currentNoiseFloor = NOISE_FLOOR_DEFAULT; + + /** + * Pure virtual hook for derived radio interfaces to provide instantaneous RSSI. + * Implementations should return dBm, or an invalid value that updateNoiseFloor() + * can reject. + */ + virtual int16_t getCurrentRSSI() = 0; + public: /** Our ISR code currently needs this to find our active instance */ static RadioLibInterface *instance; + /** + * Get the current calculated noise floor in dBm + * Returns -120 dBm if not yet calibrated + */ + int32_t getNoiseFloor(); + + /** + * Calculate the average noise floor from collected samples + */ + int32_t getAverageNoiseFloor(); + /** * Glue functions called from ISR land */ @@ -136,6 +167,8 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy, PhysicalLayer *iface = NULL); + virtual bool reconfigure() override; + virtual ErrorCode send(meshtastic_MeshPacket *p) override; /** @@ -172,6 +205,28 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ virtual bool findInTxQueue(NodeNum from, PacketId id) override; + /** + * Update the noise floor measurement by sampling RSSI from a slow path. + * This should not be called from radio interrupt or TX/RX critical paths. + */ + void updateNoiseFloor(); + + /** + * Check if we have collected any noise floor samples + */ + bool hasNoiseFloorSamples(); + + /** + * Get the number of samples in the rolling window + */ + uint8_t getNoiseFloorSampleCount(); + + /** + * Reset the noise floor calibration + * Will automatically restart collection + */ + void resetNoiseFloor(); + /** * Request randomness sourced from the LoRa modem, if supported by the active RadioLib interface. * @return true if len bytes were produced, false otherwise. @@ -179,6 +234,9 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified bool randomBytes(uint8_t *buffer, size_t length); private: + uint8_t getNoiseFloorSampleCountInternal() const; + int32_t getAverageNoiseFloorInternal() const; + /** if we have something waiting to send, start a short (random) timer so we can come check for collision before actually * doing the transmit */ void setTransmitDelay(); @@ -286,4 +344,13 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override; void checkRxDoneIrqFlag(); + + /** Software-poll substitute for a hardware DIO interrupt, for radios whose IRQ line sits behind + * an I2C IO expander with no INT routed to the MCU (e.g. Meshnology W10, LORA_DIO1_SOFTWARE_POLL). + * The chip-specific subclass polls the radio's IRQ status register from the radio thread and + * synthesizes ISR_TX/ISR_RX events equivalent to the hardware DIO1 interrupt. */ + void deliverPendingIrqFromPoll(PendingISR cause); + void scheduleIrqPollTick(); + static bool isIsrTxCallback(void (*callback)()); + virtual void handleSoftwareLoraIrqPoll() {} }; diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index bb24b365e54..96818339c24 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -351,10 +351,14 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) } fixPriority(p); // Before encryption, fix the priority if it's unset - if (!applyPositionPrecisionForChannel(*p, p->channel)) { - LOG_ERROR("Dropping malformed position packet before send"); - packetPool.release(p); - return meshtastic_Routing_Error_BAD_REQUEST; + // Position precision is an originator-only privacy policy. Relays keep + // p->from as the original sender, so do not rewrite their POSITION_APP payload. + if (isFromUs(p)) { + if (!applyPositionPrecisionForChannel(*p, p->channel)) { + LOG_ERROR("Dropping malformed position packet before send"); + packetPool.release(p); + return meshtastic_Routing_Error_BAD_REQUEST; + } } // If the packet is not yet encrypted, do so now diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index e777f204dfc..f7da5ce0fbb 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -52,8 +52,8 @@ template bool SX126xInterface::init() #ifdef SX126X_POWER_EN // Perhaps add RADIOLIB_NC check, and beforehand define as such if it is undefined, but it is not commonly // used and not part of the 'default' set of pin definitions. - digitalWrite(SX126X_POWER_EN, HIGH); pinMode(SX126X_POWER_EN, OUTPUT); + digitalWrite(SX126X_POWER_EN, HIGH); #endif #if HAS_LORA_FEM @@ -65,8 +65,8 @@ template bool SX126xInterface::init() #endif #ifdef RF95_FAN_EN - digitalWrite(RF95_FAN_EN, HIGH); pinMode(RF95_FAN_EN, OUTPUT); + digitalWrite(RF95_FAN_EN, HIGH); #endif #if ARCH_PORTDUINO @@ -200,6 +200,12 @@ template bool SX126xInterface::init() if (res == RADIOLIB_ERR_NONE) res = lora.setCRC(RADIOLIB_SX126X_LORA_CRC_ON); +#ifdef SX126X_NO_POWER_OPTIMIZATION_TABLE + // begin() applied the optimization table; re-apply the fixed PA config. + if (res == RADIOLIB_ERR_NONE) + res = lora.setOutputPower(power, false); +#endif + if (res == RADIOLIB_ERR_NONE) startReceive(); // start receiving @@ -248,21 +254,82 @@ template bool SX126xInterface::reconfigure() if (power > SX126X_MAX_POWER) // This chip has lower power limits than some power = SX126X_MAX_POWER; +#ifdef SX126X_NO_POWER_OPTIMIZATION_TABLE + err = lora.setOutputPower(power, false); // external PA: fixed PA config +#else err = lora.setOutputPower(power); +#endif if (err != RADIOLIB_ERR_NONE) LOG_ERROR("SX126X setOutputPower %s%d", radioLibErr, err); assert(err == RADIOLIB_ERR_NONE); startReceive(); // restart receiving - return RADIOLIB_ERR_NONE; + return true; +} + +template int16_t SX126xInterface::getCurrentRSSI() +{ + float rssi = lora.getRSSI(false); + return (int16_t)round(rssi); +} + +template void SX126xInterface::enableInterrupt(void (*callback)()) +{ +#ifdef LORA_DIO1_SOFTWARE_POLL + irqPollingActive = true; + pollTxMode = isIsrTxCallback(callback); + scheduleIrqPollTick(); +#else + lora.setDio1Action(callback); +#endif } template void SX126xInterface::disableInterrupt() { +#ifdef LORA_DIO1_SOFTWARE_POLL + irqPollingActive = false; +#else lora.clearDio1Action(); +#endif } +#ifdef LORA_DIO1_SOFTWARE_POLL +template void SX126xInterface::handleSoftwareLoraIrqPoll() +{ + if (!irqPollingActive) + return; + + // getIrqFlags()/clearIrqFlags() both operate on the raw SX126x IRQ register, so use the + // chip-specific RADIOLIB_SX126X_IRQ_* masks on both the read and the clear. + uint16_t irq = lora.getIrqFlags(); + const uint16_t rxEventMask = + RADIOLIB_SX126X_IRQ_RX_DONE | RADIOLIB_SX126X_IRQ_TIMEOUT | RADIOLIB_SX126X_IRQ_CRC_ERR | RADIOLIB_SX126X_IRQ_HEADER_ERR; + const uint16_t noisyRxMask = RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID; + + // Do NOT treat a preamble/header-only IRQ as a full RX event: noisy preamble detections would + // repeatedly trigger readData() and starve TX scheduling. Clear these non-terminal bits, or the + // poll loop spins at high rate while they stay latched. + if (!pollTxMode && (irq & noisyRxMask) && ((irq & ~noisyRxMask) == 0U)) { + lora.clearIrqFlags(noisyRxMask); + scheduleIrqPollTick(); + return; + } + + if (pollTxMode) { + if (irq & (RADIOLIB_SX126X_IRQ_TX_DONE | RADIOLIB_SX126X_IRQ_TIMEOUT)) { + deliverPendingIrqFromPoll(ISR_TX); + return; + } + } else if (irq & rxEventMask) { + deliverPendingIrqFromPoll(ISR_RX); + return; + } + + scheduleIrqPollTick(); +} +#endif + template void SX126xInterface::setStandby() { checkNotification(); // handle any pending interrupts before we force standby @@ -473,4 +540,4 @@ template void SX126xInterface::setTransmitEnable(bool txon) #endif } -#endif \ No newline at end of file +#endif diff --git a/src/mesh/SX126xInterface.h b/src/mesh/SX126xInterface.h index 67625e1154a..0bf977ba2ae 100644 --- a/src/mesh/SX126xInterface.h +++ b/src/mesh/SX126xInterface.h @@ -2,6 +2,7 @@ #if RADIOLIB_EXCLUDE_SX126X != 1 #include "RadioLibInterface.h" +#include "configuration.h" /** * \brief Adapter for SX126x radio family. Implements common logic for child classes. @@ -41,6 +42,8 @@ template class SX126xInterface : public RadioLibInterface */ T lora; + int16_t getCurrentRSSI() override; + /** * Glue functions called from ISR land */ @@ -49,7 +52,11 @@ template class SX126xInterface : public RadioLibInterface /** * Enable a particular ISR callback glue function */ - virtual void enableInterrupt(void (*callback)()) { lora.setDio1Action(callback); } + virtual void enableInterrupt(void (*callback)()) override; + +#ifdef LORA_DIO1_SOFTWARE_POLL + void handleSoftwareLoraIrqPoll() override; +#endif /** can we detect a LoRa preamble on the current channel? */ virtual bool isChannelActive() override; @@ -77,6 +84,10 @@ template class SX126xInterface : public RadioLibInterface uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } private: +#ifdef LORA_DIO1_SOFTWARE_POLL + bool irqPollingActive = false; + bool pollTxMode = false; +#endif /** Some boards require GPIO control of tx vs rx paths */ void setTransmitEnable(bool txon); }; diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 0e882ef05d0..7d52602568b 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -154,7 +154,7 @@ template bool SX128xInterface::reconfigure() startReceive(); // restart receiving - return RADIOLIB_ERR_NONE; + return true; } template void SX128xInterface::disableInterrupt() @@ -326,4 +326,10 @@ template bool SX128xInterface::sleep() return true; } + +template int16_t SX128xInterface::getCurrentRSSI() +{ + float rssi = lora.getRSSI(false); + return (int16_t)round(rssi); +} #endif \ No newline at end of file diff --git a/src/mesh/SX128xInterface.h b/src/mesh/SX128xInterface.h index acdcbbb27c8..cf44bcb89ce 100644 --- a/src/mesh/SX128xInterface.h +++ b/src/mesh/SX128xInterface.h @@ -35,6 +35,8 @@ template class SX128xInterface : public RadioLibInterface */ T lora; + int16_t getCurrentRSSI() override; + /** * Glue functions called from ISR land */ diff --git a/src/mesh/generated/meshtastic/atak.pb.cpp b/src/mesh/generated/meshtastic/atak.pb.cpp index dda9fddaf99..717727fa55f 100644 --- a/src/mesh/generated/meshtastic/atak.pb.cpp +++ b/src/mesh/generated/meshtastic/atak.pb.cpp @@ -30,7 +30,7 @@ PB_BIND(meshtastic_AircraftTrack, meshtastic_AircraftTrack, AUTO) PB_BIND(meshtastic_CotGeoPoint, meshtastic_CotGeoPoint, AUTO) -PB_BIND(meshtastic_DrawnShape, meshtastic_DrawnShape, 2) +PB_BIND(meshtastic_DrawnShape, meshtastic_DrawnShape, AUTO) PB_BIND(meshtastic_Marker, meshtastic_Marker, AUTO) @@ -63,6 +63,15 @@ PB_BIND(meshtastic_TAKEnvironment, meshtastic_TAKEnvironment, AUTO) PB_BIND(meshtastic_SensorFov, meshtastic_SensorFov, AUTO) +PB_BIND(meshtastic_TakTalkMessage, meshtastic_TakTalkMessage, AUTO) + + +PB_BIND(meshtastic_TakTalkRoomData, meshtastic_TakTalkRoomData, AUTO) + + +PB_BIND(meshtastic_Marti, meshtastic_Marti, AUTO) + + PB_BIND(meshtastic_TAKPacketV2, meshtastic_TAKPacketV2, 2) diff --git a/src/mesh/generated/meshtastic/atak.pb.h b/src/mesh/generated/meshtastic/atak.pb.h index d69b14009aa..6ea298f9ed7 100644 --- a/src/mesh/generated/meshtastic/atak.pb.h +++ b/src/mesh/generated/meshtastic/atak.pb.h @@ -325,7 +325,15 @@ typedef enum _meshtastic_CotType { meshtastic_CotType_CotType_b_a_o_c = 123, /* t-s: Task / engage request. Structured payload carried via the new TaskRequest typed variant. */ - meshtastic_CotType_CotType_t_s = 124 + meshtastic_CotType_CotType_t_s = 124, + /* m-t-t: TAKTALK voice/text chat message. Payload carried via the + TakTalkMessage typed variant (text, chatroom_id, lang, from_voice). */ + meshtastic_CotType_CotType_m_t_t = 125, + /* y-: TAKTALK room/membership broadcast. Payload carried via the + TakTalkRoomData typed variant (sender_callsign, room_id, room_name, + participants). The CoT type literally has a trailing dash and no + second atom — not a typo. */ + meshtastic_CotType_CotType_y = 126 } meshtastic_CotType; /* Geopoint and altitude source */ @@ -553,6 +561,18 @@ typedef struct _meshtastic_GeoChat { /* Receipt kind discriminator. See ReceiptType doc. Default ReceiptType_None means this is a regular chat message, not a receipt. */ meshtastic_GeoChat_ReceiptType receipt_type; + /* BCP-47-ish language tag or human-readable name (e.g. "en", "English") + that the originator's TAKTALK plugin recorded for the message. */ + pb_callback_t lang; + /* TAKTALK chatroom UUID (e.g. "30b2755c-c547-44ef-a0cc-cdbd8a15616f") that + the receiver's TAKTALK plugin uses to thread the message under the + right room. Resolved to a friendly name via TakTalkRoomData broadcasts. */ + pb_callback_t room_id; + /* TAKTALK voice profile pointer. Often empty in practice (the empty + marker `` still signals TAKTALK origination), so + receivers should treat empty-but-present as the equivalent of the + marker rather than a missing field. */ + pb_callback_t voice_profile_id; } meshtastic_GeoChat; /* ATAK Group @@ -715,12 +735,7 @@ typedef struct _meshtastic_DrawnShape { uint32_t fill_argb; /* Whether labels are rendered on this shape. */ bool labels_on; - /* Vertex list for polyline/polygon/rectangle shapes. Capped at 32 by - the nanopb pool; senders MUST truncate longer inputs and set - `truncated = true`. */ - pb_size_t vertices_count; - meshtastic_CotGeoPoint vertices[32]; - /* True if the sender truncated `vertices` to fit the pool. */ + /* True if the sender truncated the vertex columns to fit the pool. */ bool truncated; /* --- Bullseye-only fields. All ignored unless kind == Kind_Bullseye. --- */ /* Bullseye distance in meters * 10 (e.g. 3285 = 328.5 m). 0 = unset. */ uint32_t bullseye_distance_dm; @@ -734,6 +749,8 @@ typedef struct _meshtastic_DrawnShape { uint8_t bullseye_flags; /* Bullseye reference UID (anchor marker). Empty = anchor is self. */ char bullseye_uid_ref[48]; + pb_callback_t vertex_lat_deltas; + pb_callback_t vertex_lon_deltas; } meshtastic_DrawnShape; /* Fixed point of interest: spot marker, waypoint, checkpoint, 2525 symbol, @@ -1068,6 +1085,87 @@ typedef struct _meshtastic_SensorFov { pb_callback_t model; } meshtastic_SensorFov; +/* TAKTALK chat message payload (CoT type m-t-t). + + TAKTALK is an ATAK plugin for voice + text team messaging. The voice + audio stream goes over UDP/RTP and is NOT carried by the mesh — only + the text envelope (this message) is. `from_voice` marks messages sent + via push-to-talk speech-to-text so receivers can render a mic icon + next to the text. + + Wire shape inside /: + ... - mapped to TAKPacketV2.callsign + English - lang + ... - text + 1 - chatroom_id + - presence sets from_voice = true */ +typedef struct _meshtastic_TakTalkMessage { + /* The text body of the TAKTALK message (speech-to-text transcript when + from_voice = true, typed message otherwise). */ + pb_callback_t text; + /* TAKTALK chatroom identifier. May be a short id like "1" for the + default room or a UUID like "30b2755c-c547-44ef-a0cc-cdbd8a15616f" + for custom rooms (resolved by TakTalkRoomData broadcasts). + Empty = broadcast room. */ + pb_callback_t chatroom_id; + /* BCP-47-ish language tag or human-readable name (e.g. "en", "English"). + Empty = unspecified. */ + pb_callback_t lang; + /* True when the source CoT carried a marker, i.e. the message + originated as push-to-talk speech-to-text. Lets receivers show a mic + icon. Proto3 only encodes when true so empty payload cost is 0 bytes. */ + bool from_voice; +} meshtastic_TakTalkMessage; + +/* TAKTALK room/membership broadcast (CoT type y-). + + Announces a TAKTALK chatroom's friendly name and roster so peers can + resolve room UUIDs (used in TakTalkMessage.chatroom_id and + GeoChat.room_id) to a display name and participant list. Not a chat + message itself — these events are emitted by TAKTALK when rooms are + created or memberships change. */ +typedef struct _meshtastic_TakTalkRoomData { + /* Callsign of the device broadcasting the room state (typically the + room owner / latest writer). + + DEPRECATED in v0.3.2: always equals TAKPacketV2.callsign, so the wire + byte was redundant. Builders stop emitting this field in v0.3.2; + parsers still read it for one release so v0.3.1-encoded packets decode + cleanly. To be removed entirely in v0.4.x. */ + pb_callback_t sender_callsign; + /* Room UUID, matches TakTalkMessage.chatroom_id / GeoChat.room_id on + messages routed into this room. */ + pb_callback_t room_id; + /* Friendly display name for the room (e.g. "test", "Alpha Team"). */ + pb_callback_t room_name; + /* Member callsigns. Wire-encoded as repeated strings; the underlying + CoT carries them as a single A,B,C element + which parsers split / builders join on ','. */ + pb_callback_t participants; +} meshtastic_TakTalkRoomData; + +/* ATAK directed-routing recipient list (CoT ). + + Present when an event is addressed to specific TAK users rather than the + broadcast group. TAKTALK gates voice TTS on this element matching the + receiver's callsign; directed b-t-f chats use it for the same purpose. A + missing means "broadcast to all peers", which is the default for + PLI, alerts, drawings, and most situational-awareness events. + + Carried as repeated strings (not indexes into a per-packet table) because + the typical event has 1-2 destinations and table overhead would erase the + savings. Receivers that need the original XML element rebuild it from + dest_callsign on emit. */ +typedef struct _meshtastic_Marti { + /* Recipient callsigns. Order is preserved end-to-end so receivers can show + primary-vs-cc distinction the same way ATAK does. + + If dest_callsign is [TAKPacketV2.callsign] (self-addressed, unusual but + legal — e.g. ATAK echoing back to its own room), the builder still emits + the element so loopback shapes round-trip cleanly. */ + pb_callback_t dest_callsign; +} meshtastic_Marti; + typedef PB_BYTES_ARRAY_T(220) meshtastic_TAKPacketV2_raw_detail_t; /* ATAK v2 packet with expanded CoT field support and zstd dictionary compression. Sent on ATAK_PLUGIN_V2 port. The wire payload is: @@ -1089,7 +1187,14 @@ typedef struct _meshtastic_TAKPacketV2 { int32_t latitude_i; /* Longitude, multiply by 1e-7 to get degrees in floating point */ int32_t longitude_i; - /* Altitude in meters (HAE) */ + /* Altitude in meters (HAE). ATAK's "no altitude" sentinel is hae=9999999.0. + + NOTE: an earlier v0.4.0 attempt made this `optional` to omit the 9999999 + sentinel from the wire, but measurement showed it was net-negative: the + zstd dictionary already compresses the literal 9999999 to ~nothing, while + proto3 `optional` forces a genuine 0 m HAE (common on routes/drawings that + carry hae="0.0" or omit hae → parsed as 0) to encode explicitly (+2 bytes), + which REGRESSED the worst-case route fixture. Kept as a plain field. */ int32_t altitude; /* Speed in cm/s */ uint32_t speed; @@ -1135,10 +1240,18 @@ typedef struct _meshtastic_TAKPacketV2 { /* Sensor field-of-view cone (camera, FLIR, laser, etc.). From . */ bool has_sensor_fov; meshtastic_SensorFov sensor_fov; + /* Directed-routing recipient list (CoT ). + Empty / unset = broadcast to all peers (the default for situational-awareness + events). Populated for TAKTALK m-t-t, directed b-t-f DMs, and any other CoT + shape that ATAK addresses to specific recipients. TAKTALK gates voice TTS + playback on this element matching the receiver's callsign, so dropping it + silently breaks voice messaging end-to-end. + + See Marti. */ + bool has_marti; + meshtastic_Marti marti; pb_size_t which_payload_variant; union { - /* Position report (true = PLI, no extra fields beyond the common ones above) */ - bool pli; /* ATAK GeoChat message */ meshtastic_GeoChat chat; /* Aircraft track data (ADS-B, military air) */ @@ -1163,6 +1276,14 @@ typedef struct _meshtastic_TAKPacketV2 { meshtastic_EmergencyAlert emergency; /* Task / engage request. See TaskRequest. */ meshtastic_TaskRequest task; + /* TAKTALK chat message (CoT type m-t-t). See TakTalkMessage. + Voice audio itself rides UDP/RTP outside the mesh; this carries the + text envelope plus a from_voice marker for receiver UX. */ + meshtastic_TakTalkMessage taktalk; + /* TAKTALK room/membership broadcast (CoT type y-). See TakTalkRoomData. + Resolves room UUIDs (used in TakTalkMessage.chatroom_id and + GeoChat.room_id) to display name + roster on receivers. */ + meshtastic_TakTalkRoomData taktalk_room; } payload_variant; } meshtastic_TAKPacketV2; @@ -1185,8 +1306,8 @@ extern "C" { #define _meshtastic_CotHow_ARRAYSIZE ((meshtastic_CotHow)(meshtastic_CotHow_CotHow_m_s+1)) #define _meshtastic_CotType_MIN meshtastic_CotType_CotType_Other -#define _meshtastic_CotType_MAX meshtastic_CotType_CotType_t_s -#define _meshtastic_CotType_ARRAYSIZE ((meshtastic_CotType)(meshtastic_CotType_CotType_t_s+1)) +#define _meshtastic_CotType_MAX meshtastic_CotType_CotType_y +#define _meshtastic_CotType_ARRAYSIZE ((meshtastic_CotType)(meshtastic_CotType_CotType_y+1)) #define _meshtastic_GeoPointSource_MIN meshtastic_GeoPointSource_GeoPointSource_Unspecified #define _meshtastic_GeoPointSource_MAX meshtastic_GeoPointSource_GeoPointSource_NETWORK @@ -1282,6 +1403,9 @@ extern "C" { #define meshtastic_SensorFov_type_ENUMTYPE meshtastic_SensorFov_SensorType + + + #define meshtastic_TAKPacketV2_cot_type_id_ENUMTYPE meshtastic_CotType #define meshtastic_TAKPacketV2_how_ENUMTYPE meshtastic_CotHow #define meshtastic_TAKPacketV2_team_ENUMTYPE meshtastic_Team @@ -1292,14 +1416,14 @@ extern "C" { /* Initializer values for message structs */ #define meshtastic_TAKPacket_init_default {0, false, meshtastic_Contact_init_default, false, meshtastic_Group_init_default, false, meshtastic_Status_init_default, 0, {meshtastic_PLI_init_default}} -#define meshtastic_GeoChat_init_default {"", false, "", false, "", "", _meshtastic_GeoChat_ReceiptType_MIN} +#define meshtastic_GeoChat_init_default {"", false, "", false, "", "", _meshtastic_GeoChat_ReceiptType_MIN, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}} #define meshtastic_Group_init_default {_meshtastic_MemberRole_MIN, _meshtastic_Team_MIN} #define meshtastic_Status_init_default {0} #define meshtastic_Contact_init_default {"", ""} #define meshtastic_PLI_init_default {0, 0, 0, 0, 0} #define meshtastic_AircraftTrack_init_default {"", "", "", "", 0, "", 0, 0, ""} #define meshtastic_CotGeoPoint_init_default {0, 0} -#define meshtastic_DrawnShape_init_default {_meshtastic_DrawnShape_Kind_MIN, _meshtastic_DrawnShape_StyleMode_MIN, 0, 0, 0, _meshtastic_Team_MIN, 0, 0, _meshtastic_Team_MIN, 0, 0, 0, {meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default}, 0, 0, 0, 0, ""} +#define meshtastic_DrawnShape_init_default {_meshtastic_DrawnShape_Kind_MIN, _meshtastic_DrawnShape_StyleMode_MIN, 0, 0, 0, _meshtastic_Team_MIN, 0, 0, _meshtastic_Team_MIN, 0, 0, 0, 0, 0, 0, "", {{NULL}, NULL}, {{NULL}, NULL}} #define meshtastic_Marker_init_default {_meshtastic_Marker_Kind_MIN, _meshtastic_Team_MIN, 0, 0, "", "", "", ""} #define meshtastic_RangeAndBearing_init_default {false, meshtastic_CotGeoPoint_init_default, "", 0, 0, _meshtastic_Team_MIN, 0, 0} #define meshtastic_Route_init_default {_meshtastic_Route_Method_MIN, _meshtastic_Route_Direction_MIN, "", 0, 0, {meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default}, 0} @@ -1310,16 +1434,19 @@ extern "C" { #define meshtastic_TaskRequest_init_default {"", "", "", _meshtastic_TaskRequest_Priority_MIN, _meshtastic_TaskRequest_Status_MIN, ""} #define meshtastic_TAKEnvironment_init_default {0, 0, 0} #define meshtastic_SensorFov_init_default {_meshtastic_SensorFov_SensorType_MIN, 0, false, 0, 0, 0, 0, 0, {{NULL}, NULL}} -#define meshtastic_TAKPacketV2_init_default {_meshtastic_CotType_MIN, _meshtastic_CotHow_MIN, "", _meshtastic_Team_MIN, _meshtastic_MemberRole_MIN, 0, 0, 0, 0, 0, 0, _meshtastic_GeoPointSource_MIN, _meshtastic_GeoPointSource_MIN, "", "", 0, "", "", "", "", "", "", "", {{NULL}, NULL}, false, meshtastic_TAKEnvironment_init_default, false, meshtastic_SensorFov_init_default, 0, {0}} +#define meshtastic_TakTalkMessage_init_default {{{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, 0} +#define meshtastic_TakTalkRoomData_init_default {{{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}} +#define meshtastic_Marti_init_default {{{NULL}, NULL}} +#define meshtastic_TAKPacketV2_init_default {_meshtastic_CotType_MIN, _meshtastic_CotHow_MIN, "", _meshtastic_Team_MIN, _meshtastic_MemberRole_MIN, 0, 0, 0, 0, 0, 0, _meshtastic_GeoPointSource_MIN, _meshtastic_GeoPointSource_MIN, "", "", 0, "", "", "", "", "", "", "", {{NULL}, NULL}, false, meshtastic_TAKEnvironment_init_default, false, meshtastic_SensorFov_init_default, false, meshtastic_Marti_init_default, 0, {meshtastic_GeoChat_init_default}} #define meshtastic_TAKPacket_init_zero {0, false, meshtastic_Contact_init_zero, false, meshtastic_Group_init_zero, false, meshtastic_Status_init_zero, 0, {meshtastic_PLI_init_zero}} -#define meshtastic_GeoChat_init_zero {"", false, "", false, "", "", _meshtastic_GeoChat_ReceiptType_MIN} +#define meshtastic_GeoChat_init_zero {"", false, "", false, "", "", _meshtastic_GeoChat_ReceiptType_MIN, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}} #define meshtastic_Group_init_zero {_meshtastic_MemberRole_MIN, _meshtastic_Team_MIN} #define meshtastic_Status_init_zero {0} #define meshtastic_Contact_init_zero {"", ""} #define meshtastic_PLI_init_zero {0, 0, 0, 0, 0} #define meshtastic_AircraftTrack_init_zero {"", "", "", "", 0, "", 0, 0, ""} #define meshtastic_CotGeoPoint_init_zero {0, 0} -#define meshtastic_DrawnShape_init_zero {_meshtastic_DrawnShape_Kind_MIN, _meshtastic_DrawnShape_StyleMode_MIN, 0, 0, 0, _meshtastic_Team_MIN, 0, 0, _meshtastic_Team_MIN, 0, 0, 0, {meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero}, 0, 0, 0, 0, ""} +#define meshtastic_DrawnShape_init_zero {_meshtastic_DrawnShape_Kind_MIN, _meshtastic_DrawnShape_StyleMode_MIN, 0, 0, 0, _meshtastic_Team_MIN, 0, 0, _meshtastic_Team_MIN, 0, 0, 0, 0, 0, 0, "", {{NULL}, NULL}, {{NULL}, NULL}} #define meshtastic_Marker_init_zero {_meshtastic_Marker_Kind_MIN, _meshtastic_Team_MIN, 0, 0, "", "", "", ""} #define meshtastic_RangeAndBearing_init_zero {false, meshtastic_CotGeoPoint_init_zero, "", 0, 0, _meshtastic_Team_MIN, 0, 0} #define meshtastic_Route_init_zero {_meshtastic_Route_Method_MIN, _meshtastic_Route_Direction_MIN, "", 0, 0, {meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero}, 0} @@ -1330,7 +1457,10 @@ extern "C" { #define meshtastic_TaskRequest_init_zero {"", "", "", _meshtastic_TaskRequest_Priority_MIN, _meshtastic_TaskRequest_Status_MIN, ""} #define meshtastic_TAKEnvironment_init_zero {0, 0, 0} #define meshtastic_SensorFov_init_zero {_meshtastic_SensorFov_SensorType_MIN, 0, false, 0, 0, 0, 0, 0, {{NULL}, NULL}} -#define meshtastic_TAKPacketV2_init_zero {_meshtastic_CotType_MIN, _meshtastic_CotHow_MIN, "", _meshtastic_Team_MIN, _meshtastic_MemberRole_MIN, 0, 0, 0, 0, 0, 0, _meshtastic_GeoPointSource_MIN, _meshtastic_GeoPointSource_MIN, "", "", 0, "", "", "", "", "", "", "", {{NULL}, NULL}, false, meshtastic_TAKEnvironment_init_zero, false, meshtastic_SensorFov_init_zero, 0, {0}} +#define meshtastic_TakTalkMessage_init_zero {{{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, 0} +#define meshtastic_TakTalkRoomData_init_zero {{{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}} +#define meshtastic_Marti_init_zero {{{NULL}, NULL}} +#define meshtastic_TAKPacketV2_init_zero {_meshtastic_CotType_MIN, _meshtastic_CotHow_MIN, "", _meshtastic_Team_MIN, _meshtastic_MemberRole_MIN, 0, 0, 0, 0, 0, 0, _meshtastic_GeoPointSource_MIN, _meshtastic_GeoPointSource_MIN, "", "", 0, "", "", "", "", "", "", "", {{NULL}, NULL}, false, meshtastic_TAKEnvironment_init_zero, false, meshtastic_SensorFov_init_zero, false, meshtastic_Marti_init_zero, 0, {meshtastic_GeoChat_init_zero}} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_GeoChat_message_tag 1 @@ -1338,6 +1468,9 @@ extern "C" { #define meshtastic_GeoChat_to_callsign_tag 3 #define meshtastic_GeoChat_receipt_for_uid_tag 4 #define meshtastic_GeoChat_receipt_type_tag 5 +#define meshtastic_GeoChat_lang_tag 6 +#define meshtastic_GeoChat_room_id_tag 7 +#define meshtastic_GeoChat_voice_profile_id_tag 8 #define meshtastic_Group_role_tag 1 #define meshtastic_Group_team_tag 2 #define meshtastic_Status_battery_tag 1 @@ -1377,12 +1510,13 @@ extern "C" { #define meshtastic_DrawnShape_fill_color_tag 9 #define meshtastic_DrawnShape_fill_argb_tag 10 #define meshtastic_DrawnShape_labels_on_tag 11 -#define meshtastic_DrawnShape_vertices_tag 12 #define meshtastic_DrawnShape_truncated_tag 13 #define meshtastic_DrawnShape_bullseye_distance_dm_tag 14 #define meshtastic_DrawnShape_bullseye_bearing_ref_tag 15 #define meshtastic_DrawnShape_bullseye_flags_tag 16 #define meshtastic_DrawnShape_bullseye_uid_ref_tag 17 +#define meshtastic_DrawnShape_vertex_lat_deltas_tag 18 +#define meshtastic_DrawnShape_vertex_lon_deltas_tag 19 #define meshtastic_Marker_kind_tag 1 #define meshtastic_Marker_color_tag 2 #define meshtastic_Marker_color_argb_tag 3 @@ -1467,6 +1601,15 @@ extern "C" { #define meshtastic_SensorFov_elevation_deg_tag 6 #define meshtastic_SensorFov_roll_deg_tag 7 #define meshtastic_SensorFov_model_tag 8 +#define meshtastic_TakTalkMessage_text_tag 1 +#define meshtastic_TakTalkMessage_chatroom_id_tag 2 +#define meshtastic_TakTalkMessage_lang_tag 3 +#define meshtastic_TakTalkMessage_from_voice_tag 4 +#define meshtastic_TakTalkRoomData_sender_callsign_tag 1 +#define meshtastic_TakTalkRoomData_room_id_tag 2 +#define meshtastic_TakTalkRoomData_room_name_tag 3 +#define meshtastic_TakTalkRoomData_participants_tag 4 +#define meshtastic_Marti_dest_callsign_tag 1 #define meshtastic_TAKPacketV2_cot_type_id_tag 1 #define meshtastic_TAKPacketV2_how_tag 2 #define meshtastic_TAKPacketV2_callsign_tag 3 @@ -1493,7 +1636,7 @@ extern "C" { #define meshtastic_TAKPacketV2_remarks_tag 24 #define meshtastic_TAKPacketV2_environment_tag 25 #define meshtastic_TAKPacketV2_sensor_fov_tag 26 -#define meshtastic_TAKPacketV2_pli_tag 30 +#define meshtastic_TAKPacketV2_marti_tag 29 #define meshtastic_TAKPacketV2_chat_tag 31 #define meshtastic_TAKPacketV2_aircraft_tag 32 #define meshtastic_TAKPacketV2_raw_detail_tag 33 @@ -1504,6 +1647,8 @@ extern "C" { #define meshtastic_TAKPacketV2_casevac_tag 38 #define meshtastic_TAKPacketV2_emergency_tag 39 #define meshtastic_TAKPacketV2_task_tag 40 +#define meshtastic_TAKPacketV2_taktalk_tag 41 +#define meshtastic_TAKPacketV2_taktalk_room_tag 42 /* Struct field encoding specification for nanopb */ #define meshtastic_TAKPacket_FIELDLIST(X, a) \ @@ -1527,8 +1672,11 @@ X(a, STATIC, SINGULAR, STRING, message, 1) \ X(a, STATIC, OPTIONAL, STRING, to, 2) \ X(a, STATIC, OPTIONAL, STRING, to_callsign, 3) \ X(a, STATIC, SINGULAR, STRING, receipt_for_uid, 4) \ -X(a, STATIC, SINGULAR, UENUM, receipt_type, 5) -#define meshtastic_GeoChat_CALLBACK NULL +X(a, STATIC, SINGULAR, UENUM, receipt_type, 5) \ +X(a, CALLBACK, OPTIONAL, STRING, lang, 6) \ +X(a, CALLBACK, OPTIONAL, STRING, room_id, 7) \ +X(a, CALLBACK, OPTIONAL, STRING, voice_profile_id, 8) +#define meshtastic_GeoChat_CALLBACK pb_default_field_callback #define meshtastic_GeoChat_DEFAULT NULL #define meshtastic_Group_FIELDLIST(X, a) \ @@ -1588,15 +1736,15 @@ X(a, STATIC, SINGULAR, UINT32, stroke_weight_x10, 8) \ X(a, STATIC, SINGULAR, UENUM, fill_color, 9) \ X(a, STATIC, SINGULAR, FIXED32, fill_argb, 10) \ X(a, STATIC, SINGULAR, BOOL, labels_on, 11) \ -X(a, STATIC, REPEATED, MESSAGE, vertices, 12) \ X(a, STATIC, SINGULAR, BOOL, truncated, 13) \ X(a, STATIC, SINGULAR, UINT32, bullseye_distance_dm, 14) \ X(a, STATIC, SINGULAR, UINT32, bullseye_bearing_ref, 15) \ X(a, STATIC, SINGULAR, UINT32, bullseye_flags, 16) \ -X(a, STATIC, SINGULAR, STRING, bullseye_uid_ref, 17) -#define meshtastic_DrawnShape_CALLBACK NULL +X(a, STATIC, SINGULAR, STRING, bullseye_uid_ref, 17) \ +X(a, CALLBACK, REPEATED, SINT32, vertex_lat_deltas, 18) \ +X(a, CALLBACK, REPEATED, SINT32, vertex_lon_deltas, 19) +#define meshtastic_DrawnShape_CALLBACK pb_default_field_callback #define meshtastic_DrawnShape_DEFAULT NULL -#define meshtastic_DrawnShape_vertices_MSGTYPE meshtastic_CotGeoPoint #define meshtastic_Marker_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UENUM, kind, 1) \ @@ -1726,6 +1874,27 @@ X(a, CALLBACK, SINGULAR, STRING, model, 8) #define meshtastic_SensorFov_CALLBACK pb_default_field_callback #define meshtastic_SensorFov_DEFAULT NULL +#define meshtastic_TakTalkMessage_FIELDLIST(X, a) \ +X(a, CALLBACK, SINGULAR, STRING, text, 1) \ +X(a, CALLBACK, SINGULAR, STRING, chatroom_id, 2) \ +X(a, CALLBACK, SINGULAR, STRING, lang, 3) \ +X(a, STATIC, SINGULAR, BOOL, from_voice, 4) +#define meshtastic_TakTalkMessage_CALLBACK pb_default_field_callback +#define meshtastic_TakTalkMessage_DEFAULT NULL + +#define meshtastic_TakTalkRoomData_FIELDLIST(X, a) \ +X(a, CALLBACK, SINGULAR, STRING, sender_callsign, 1) \ +X(a, CALLBACK, SINGULAR, STRING, room_id, 2) \ +X(a, CALLBACK, SINGULAR, STRING, room_name, 3) \ +X(a, CALLBACK, REPEATED, STRING, participants, 4) +#define meshtastic_TakTalkRoomData_CALLBACK pb_default_field_callback +#define meshtastic_TakTalkRoomData_DEFAULT NULL + +#define meshtastic_Marti_FIELDLIST(X, a) \ +X(a, CALLBACK, REPEATED, STRING, dest_callsign, 1) +#define meshtastic_Marti_CALLBACK pb_default_field_callback +#define meshtastic_Marti_DEFAULT NULL + #define meshtastic_TAKPacketV2_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UENUM, cot_type_id, 1) \ X(a, STATIC, SINGULAR, UENUM, how, 2) \ @@ -1753,7 +1922,7 @@ X(a, STATIC, SINGULAR, STRING, cot_type_str, 23) \ X(a, CALLBACK, SINGULAR, STRING, remarks, 24) \ X(a, STATIC, OPTIONAL, MESSAGE, environment, 25) \ X(a, STATIC, OPTIONAL, MESSAGE, sensor_fov, 26) \ -X(a, STATIC, ONEOF, BOOL, (payload_variant,pli,payload_variant.pli), 30) \ +X(a, STATIC, OPTIONAL, MESSAGE, marti, 29) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,chat,payload_variant.chat), 31) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,aircraft,payload_variant.aircraft), 32) \ X(a, STATIC, ONEOF, BYTES, (payload_variant,raw_detail,payload_variant.raw_detail), 33) \ @@ -1763,11 +1932,14 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,rab,payload_variant.rab), 3 X(a, STATIC, ONEOF, MESSAGE, (payload_variant,route,payload_variant.route), 37) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,casevac,payload_variant.casevac), 38) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,emergency,payload_variant.emergency), 39) \ -X(a, STATIC, ONEOF, MESSAGE, (payload_variant,task,payload_variant.task), 40) +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,task,payload_variant.task), 40) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,taktalk,payload_variant.taktalk), 41) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,taktalk_room,payload_variant.taktalk_room), 42) #define meshtastic_TAKPacketV2_CALLBACK pb_default_field_callback #define meshtastic_TAKPacketV2_DEFAULT NULL #define meshtastic_TAKPacketV2_environment_MSGTYPE meshtastic_TAKEnvironment #define meshtastic_TAKPacketV2_sensor_fov_MSGTYPE meshtastic_SensorFov +#define meshtastic_TAKPacketV2_marti_MSGTYPE meshtastic_Marti #define meshtastic_TAKPacketV2_payload_variant_chat_MSGTYPE meshtastic_GeoChat #define meshtastic_TAKPacketV2_payload_variant_aircraft_MSGTYPE meshtastic_AircraftTrack #define meshtastic_TAKPacketV2_payload_variant_shape_MSGTYPE meshtastic_DrawnShape @@ -1777,6 +1949,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,task,payload_variant.task), #define meshtastic_TAKPacketV2_payload_variant_casevac_MSGTYPE meshtastic_CasevacReport #define meshtastic_TAKPacketV2_payload_variant_emergency_MSGTYPE meshtastic_EmergencyAlert #define meshtastic_TAKPacketV2_payload_variant_task_MSGTYPE meshtastic_TaskRequest +#define meshtastic_TAKPacketV2_payload_variant_taktalk_MSGTYPE meshtastic_TakTalkMessage +#define meshtastic_TAKPacketV2_payload_variant_taktalk_room_MSGTYPE meshtastic_TakTalkRoomData extern const pb_msgdesc_t meshtastic_TAKPacket_msg; extern const pb_msgdesc_t meshtastic_GeoChat_msg; @@ -1797,6 +1971,9 @@ extern const pb_msgdesc_t meshtastic_EmergencyAlert_msg; extern const pb_msgdesc_t meshtastic_TaskRequest_msg; extern const pb_msgdesc_t meshtastic_TAKEnvironment_msg; extern const pb_msgdesc_t meshtastic_SensorFov_msg; +extern const pb_msgdesc_t meshtastic_TakTalkMessage_msg; +extern const pb_msgdesc_t meshtastic_TakTalkRoomData_msg; +extern const pb_msgdesc_t meshtastic_Marti_msg; extern const pb_msgdesc_t meshtastic_TAKPacketV2_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ @@ -1819,20 +1996,27 @@ extern const pb_msgdesc_t meshtastic_TAKPacketV2_msg; #define meshtastic_TaskRequest_fields &meshtastic_TaskRequest_msg #define meshtastic_TAKEnvironment_fields &meshtastic_TAKEnvironment_msg #define meshtastic_SensorFov_fields &meshtastic_SensorFov_msg +#define meshtastic_TakTalkMessage_fields &meshtastic_TakTalkMessage_msg +#define meshtastic_TakTalkRoomData_fields &meshtastic_TakTalkRoomData_msg +#define meshtastic_Marti_fields &meshtastic_Marti_msg #define meshtastic_TAKPacketV2_fields &meshtastic_TAKPacketV2_msg /* Maximum encoded size of messages (where known) */ +/* meshtastic_TAKPacket_size depends on runtime parameters */ +/* meshtastic_GeoChat_size depends on runtime parameters */ +/* meshtastic_DrawnShape_size depends on runtime parameters */ /* meshtastic_CasevacReport_size depends on runtime parameters */ /* meshtastic_ZMistEntry_size depends on runtime parameters */ /* meshtastic_SensorFov_size depends on runtime parameters */ +/* meshtastic_TakTalkMessage_size depends on runtime parameters */ +/* meshtastic_TakTalkRoomData_size depends on runtime parameters */ +/* meshtastic_Marti_size depends on runtime parameters */ /* meshtastic_TAKPacketV2_size depends on runtime parameters */ #define MESHTASTIC_MESHTASTIC_ATAK_PB_H_MAX_SIZE meshtastic_Route_size #define meshtastic_AircraftTrack_size 134 #define meshtastic_Contact_size 242 #define meshtastic_CotGeoPoint_size 12 -#define meshtastic_DrawnShape_size 553 #define meshtastic_EmergencyAlert_size 100 -#define meshtastic_GeoChat_size 495 #define meshtastic_Group_size 4 #define meshtastic_Marker_size 191 #define meshtastic_PLI_size 31 @@ -1841,7 +2025,6 @@ extern const pb_msgdesc_t meshtastic_TAKPacketV2_msg; #define meshtastic_Route_size 1379 #define meshtastic_Status_size 3 #define meshtastic_TAKEnvironment_size 18 -#define meshtastic_TAKPacket_size 756 #define meshtastic_TaskRequest_size 132 #ifdef __cplusplus diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 820bb276450..f9ae1ff7240 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -290,15 +290,17 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode { meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26, /* ITU Region 1 Amateur Radio 2m band (144-146 MHz) */ meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M = 27, - /* ITU Region 2 / 3 Amateur Radio 2m band (144-148 MHz) */ - meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M = 28, + /* ITU Region 2 Amateur Radio 2m band (144-148 MHz) */ + meshtastic_Config_LoRaConfig_RegionCode_ITU2_2M = 28, /* EU 866MHz band (Band no. 47b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD) */ meshtastic_Config_LoRaConfig_RegionCode_EU_866 = 29, /* EU 874MHz and 917MHz bands (Band no. 1 and 4 of 2022/172/EC and subsequent amendments) for Non-specific short-range devices (SRD) */ meshtastic_Config_LoRaConfig_RegionCode_EU_874 = 30, meshtastic_Config_LoRaConfig_RegionCode_EU_917 = 31, /* EU 868MHz band, with narrow presets */ - meshtastic_Config_LoRaConfig_RegionCode_EU_N_868 = 32 + meshtastic_Config_LoRaConfig_RegionCode_EU_N_868 = 32, + /* ITU Region 3 Amateur Radio 2m band (144-148 MHz) */ + meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M = 33 } meshtastic_Config_LoRaConfig_RegionCode; /* Standard predefined channel settings @@ -734,8 +736,8 @@ extern "C" { #define _meshtastic_Config_DisplayConfig_CompassOrientation_ARRAYSIZE ((meshtastic_Config_DisplayConfig_CompassOrientation)(meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED+1)) #define _meshtastic_Config_LoRaConfig_RegionCode_MIN meshtastic_Config_LoRaConfig_RegionCode_UNSET -#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_EU_N_868 -#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868+1)) +#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M +#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M+1)) #define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST #define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index cb5f19df5a0..303981cff2c 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -308,9 +308,9 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_TDISPLAY_S3_PRO = 126, /* Heltec Mesh Node T096 board features an nRF52840 CPU and a TFT screen. */ meshtastic_HardwareModel_HELTEC_MESH_NODE_T096 = 127, - /* Seeed studio T1000-E Pro tracker card. NRF52840 w/ LR2021 radio, + /* Seeed studio Mesh Tracker X1card. NRF52840 w/ LR2021 radio, GPS, button, buzzer, and sensors. */ - meshtastic_HardwareModel_TRACKER_T1000_E_PRO = 128, + meshtastic_HardwareModel_MESH_TRACKER_X1 = 128, /* Elecrow ThinkNode M7, M8 and M9 */ meshtastic_HardwareModel_THINKNODE_M7 = 129, meshtastic_HardwareModel_THINKNODE_M8 = 130, @@ -325,6 +325,16 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_T_IMPULSE_PLUS = 135, /* Lilygo T-Echo Card */ meshtastic_HardwareModel_T_ECHO_CARD = 136, + /* Seeed Tracker L2 */ + meshtastic_HardwareModel_SEEED_WIO_TRACKER_L2 = 137, + /* Elecrow CrowPanel Advance P4 models, ESP32-P4 and TFT with SX1262 radio plugin */ + meshtastic_HardwareModel_CROWPANEL_P4 = 138, + /* Heltec Mesh Tower V2 */ + meshtastic_HardwareModel_HELTEC_MESH_TOWER_V2 = 139, + /* Meshnology W10 */ + meshtastic_HardwareModel_MESHNOLOGY_W10 = 140, + /* Seeed Wio Tracker L1 Pro 1W, nRF52840 + SX1262 with 1 W external PA */ + meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1_PRO_1W = 144, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ diff --git a/src/mesh/generated/meshtastic/telemetry.pb.h b/src/mesh/generated/meshtastic/telemetry.pb.h index e8d337049df..36f9305612d 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.h +++ b/src/mesh/generated/meshtastic/telemetry.pb.h @@ -119,7 +119,11 @@ typedef enum _meshtastic_TelemetrySensorType { /* MMC5983MA 3-Axis Digital Magnetic Sensor */ meshtastic_TelemetrySensorType_MMC5983MA = 52, /* ICM-42607-P 6‑Axis IMU */ - meshtastic_TelemetrySensorType_ICM42607P = 53 + meshtastic_TelemetrySensorType_ICM42607P = 53, + /* SPA06 pressure and temperature */ + meshtastic_TelemetrySensorType_SPA06 = 54, + /* HM330X PM SENSOR */ + meshtastic_TelemetrySensorType_HM330X = 55 } meshtastic_TelemetrySensorType; /* Struct definitions */ @@ -500,8 +504,8 @@ extern "C" { /* Helper constants for enums */ #define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET -#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_ICM42607P -#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_ICM42607P+1)) +#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_HM330X +#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_HM330X+1)) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index c6b17d5b060..6c8438bbb76 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -987,11 +987,11 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) case meshtastic_ModuleConfig_neighbor_info_tag: LOG_INFO("Set module config: Neighbor Info"); moduleConfig.has_neighbor_info = true; + moduleConfig.neighbor_info = c.payload_variant.neighbor_info; if (moduleConfig.neighbor_info.update_interval < min_neighbor_info_broadcast_secs) { LOG_DEBUG("Tried to set update_interval too low, setting to %d", default_neighbor_info_broadcast_secs); moduleConfig.neighbor_info.update_interval = default_neighbor_info_broadcast_secs; } - moduleConfig.neighbor_info = c.payload_variant.neighbor_info; break; case meshtastic_ModuleConfig_detection_sensor_tag: LOG_INFO("Set module config: Detection Sensor"); diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 65e90313444..7d528930a3c 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -83,7 +83,11 @@ void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChan // Do NOT override explicit broadcast replies // Only reuse lastDest in LaunchRepeatDestination() - dest = newDest; + if (newDest == 0) { + dest = NODENUM_BROADCAST; + } else { + dest = newDest; + } channel = newChannel; lastDest = dest; @@ -123,7 +127,11 @@ void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t // Do NOT override explicit broadcast replies // Only reuse lastDest in LaunchRepeatDestination() - dest = newDest; + if (newDest == 0) { + dest = NODENUM_BROADCAST; + } else { + dest = newDest; + } channel = newChannel; lastDest = dest; diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index 0a1c4a6dd40..1a25c8253e1 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -127,7 +127,10 @@ int32_t ExternalNotificationModule::runOnce() #endif #ifdef HAS_DRV2605 - drv.go(); + // Only trigger DRV2605 if vibration alerts are enabled + if (moduleConfig.external_notification.alert_message_vibra || moduleConfig.external_notification.alert_bell_vibra) { + drv.go(); + } #endif } @@ -144,7 +147,7 @@ int32_t ExternalNotificationModule::runOnce() } #endif // now let the PWM buzzer play - if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio && canBuzz()) { + if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio && canBuzz() && buzzerShouldAlert) { if (rtttl::isPlaying()) { rtttl::play(); } else if (isNagging && (nagCycleCutoff >= millis())) { @@ -221,13 +224,24 @@ void ExternalNotificationModule::setExternalState(uint8_t index, bool on) blue = 0; white = 0; } - ambientLightingThread->setLighting(moduleConfig.ambient_lighting.current, red, green, blue); + if (ambientLightingThread) { + ambientLightingThread->setLighting(moduleConfig.ambient_lighting.current, red, green, blue); + } #endif #ifdef HAS_DRV2605 + // Only trigger DRV2605 when setting vibration motor + bool shouldTriggerDRV = false; if (on) { + if (index == 1 && + (moduleConfig.external_notification.alert_message_vibra || moduleConfig.external_notification.alert_bell_vibra)) { + shouldTriggerDRV = true; + } + } + + if (shouldTriggerDRV) { drv.go(); - } else { + } else if (!on && index == 1) { drv.stop(); } #endif @@ -266,6 +280,7 @@ void ExternalNotificationModule::stopNow() // Prevent the state machine from immediately re-triggering outputs after a manual stop. isNagging = false; + buzzerShouldAlert = false; nagCycleCutoff = UINT32_MAX; #ifdef HAS_I2S @@ -404,8 +419,10 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP // Alert GPIO Buzzer when receiving a bell = alertBellBuzzer: true // Alert GPIO Buzzer when receiving a message = alertMessageBuzzer: true - const bool buzzerShouldAlert = canBuzz() && ((moduleConfig.external_notification.alert_bell_buzzer && containsBell) || - (moduleConfig.external_notification.alert_message_buzzer && !is_muted)); + // If you are already buzzing, keep going + buzzerShouldAlert = + buzzerShouldAlert || (canBuzz() && ((moduleConfig.external_notification.alert_bell_buzzer && containsBell) || + (moduleConfig.external_notification.alert_message_buzzer && !is_muted))); if (genericShouldAlert || vibraShouldAlert || buzzerShouldAlert) { nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout @@ -422,6 +439,18 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP if (vibraShouldAlert) { LOG_INFO("externalNotificationModule - Vibra alert"); +#ifdef HAS_DRV2605 + // Set DRV2605 waveform when vibration alert is triggered + drv.setWaveform(0, 16); // Long buzzer 100% + drv.setWaveform(1, 0); // Pause + drv.setWaveform(2, 16); + drv.setWaveform(3, 0); + drv.setWaveform(4, 16); + drv.setWaveform(5, 0); + drv.setWaveform(6, 16); + drv.setWaveform(7, 0); + drv.go(); +#endif setExternalState(1, true); } @@ -431,18 +460,6 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); } else { // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us -#ifdef HAS_DRV2605 - drv.setWaveform(0, 16); // Long buzzer 100% - drv.setWaveform(1, 0); // Pause - drv.setWaveform(2, 16); - drv.setWaveform(3, 0); - drv.setWaveform(4, 16); - drv.setWaveform(5, 0); - drv.setWaveform(6, 16); - drv.setWaveform(7, 0); - drv.go(); -#endif - if (moduleConfig.external_notification.use_i2s_as_buzzer) { #ifdef HAS_I2S audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); diff --git a/src/modules/ExternalNotificationModule.h b/src/modules/ExternalNotificationModule.h index 8781c1ca84a..03eac036bb6 100644 --- a/src/modules/ExternalNotificationModule.h +++ b/src/modules/ExternalNotificationModule.h @@ -90,6 +90,7 @@ class ExternalNotificationModule : public SinglePortModule, private concurrency: bool isNagging = false; bool isSilenced = false; + bool buzzerShouldAlert = false; virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, diff --git a/src/modules/StatusLEDModule.cpp b/src/modules/StatusLEDModule.cpp index f3a0e7a03ed..1e68b034602 100644 --- a/src/modules/StatusLEDModule.cpp +++ b/src/modules/StatusLEDModule.cpp @@ -1,6 +1,7 @@ #include "StatusLEDModule.h" #include "MeshService.h" #include "configuration.h" +#include "mesh/RadioInterface.h" #include /* @@ -17,6 +18,9 @@ StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule") if (inputBroker) inputObserver.observe(inputBroker); #endif +#ifdef LED_LORA + loraRxObserver.observe(&RadioInterface::loraRxPacketObservable); +#endif #ifdef NEOPIXEL_STATUS_POWER_PIN powerPixel.begin(); powerPixel.clear(); @@ -90,6 +94,18 @@ int StatusLEDModule::handleInputEvent(const InputEvent *event) return 0; } #endif +#ifdef LED_LORA +int StatusLEDModule::handleLoRaRx(uint32_t) +{ + // Briefly flash LED_LORA on each received packet. Turn it on now (we share the main thread with + // the radio's receive handler, so this is safe) and wake runOnce() at flash end to turn it off. + digitalWrite(LED_LORA, LED_STATE_ON); + LORA_LED_state = LED_STATE_ON; + LORA_LED_starttime = millis(); + setIntervalFromNow(LORA_RX_LED_FLASH_MS); + return 0; +} +#endif int32_t StatusLEDModule::runOnce() { @@ -115,6 +131,12 @@ int32_t StatusLEDModule::runOnce() CHARGE_LED_state = LED_STATE_OFF; } } + } else { +#if defined(LED_HEARTBEAT) + // If we are using the heartbeat, as in the Thinknode M4, we need to explicitly turn off the charge LED + // This probably implies that in the future we need to stop re-using this bool for multiple purposes. + CHARGE_LED_state = LED_STATE_OFF; +#endif } // If we want a LED to be dedicated to the simple hearbeat, we can use that instead of the charge LED #if defined(LED_HEARTBEAT) @@ -142,6 +164,7 @@ int32_t StatusLEDModule::runOnce() } } #endif +#ifdef LED_PAIRING if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis() || doing_fast_blink) { PAIRING_LED_state = LED_STATE_OFF; } else if (ble_state == unpaired) { @@ -156,6 +179,7 @@ int32_t StatusLEDModule::runOnce() } else { PAIRING_LED_state = LED_STATE_ON; } +#endif // Override if disabled in config if (config.device.led_heartbeat_disabled) { @@ -191,9 +215,29 @@ int32_t StatusLEDModule::runOnce() #ifdef PCA_LED_ENABLE io.digitalWrite(PCA_LED_ENABLE, CHARGE_LED_state); #endif + #ifdef LED_POWER +#ifdef LED_POWER_CRITICAL + // Split behavior only when the two LEDs are on distinct pins. If a board maps + // LED_POWER and LED_POWER_CRITICAL to the same GPIO, the two writes would race + // (second wins) and invert normal/critical; fall back to a single write there. + // Both are compile-time constants, so the unused branch folds away at build time. + if (LED_POWER != LED_POWER_CRITICAL) { + if (power_state == critical) { + digitalWrite(LED_POWER, 0); + digitalWrite(LED_POWER_CRITICAL, CHARGE_LED_state); + } else { + digitalWrite(LED_POWER, CHARGE_LED_state); + digitalWrite(LED_POWER_CRITICAL, 0); + } + } else { + digitalWrite(LED_POWER, CHARGE_LED_state); + } +#else digitalWrite(LED_POWER, CHARGE_LED_state); #endif +#endif + #ifdef LED_PAIRING digitalWrite(LED_PAIRING, PAIRING_LED_state); #endif @@ -227,6 +271,20 @@ int32_t StatusLEDModule::runOnce() digitalWrite(Battery_LED_4, chargeIndicatorLED4); #endif +#ifdef LED_LORA + // End the LoRa-RX flash once its duration has elapsed; otherwise make sure we come back + // exactly at flash end (only ever clamp my_interval down, so other LED timing is preserved). + if (LORA_LED_state == LED_STATE_ON) { + uint32_t elapsed = millis() - LORA_LED_starttime; + if (elapsed >= LORA_RX_LED_FLASH_MS) { + digitalWrite(LED_LORA, LED_STATE_OFF); + LORA_LED_state = LED_STATE_OFF; + } else if ((uint32_t)my_interval > LORA_RX_LED_FLASH_MS - elapsed) { + my_interval = LORA_RX_LED_FLASH_MS - elapsed; + } + } +#endif + return (my_interval); } diff --git a/src/modules/StatusLEDModule.h b/src/modules/StatusLEDModule.h index f20198e39ef..793877400e7 100644 --- a/src/modules/StatusLEDModule.h +++ b/src/modules/StatusLEDModule.h @@ -43,6 +43,9 @@ class StatusLEDModule : private concurrency::OSThread #if !MESHTASTIC_EXCLUDE_INPUTBROKER int handleInputEvent(const InputEvent *arg); #endif +#ifdef LED_LORA + int handleLoRaRx(uint32_t sender); +#endif void setPowerLED(bool); @@ -65,6 +68,10 @@ class StatusLEDModule : private concurrency::OSThread CallbackObserver inputObserver = CallbackObserver(this, &StatusLEDModule::handleInputEvent); #endif +#ifdef LED_LORA + CallbackObserver loraRxObserver = + CallbackObserver(this, &StatusLEDModule::handleLoRaRx); +#endif private: bool CHARGE_LED_state = LED_STATE_OFF; @@ -77,6 +84,11 @@ class StatusLEDModule : private concurrency::OSThread uint32_t lastUserbuttonTime = 0; uint32_t POWER_LED_starttime = 0; bool doing_fast_blink = false; +#ifdef LED_LORA + static constexpr uint32_t LORA_RX_LED_FLASH_MS = 100; + bool LORA_LED_state = LED_STATE_OFF; + uint32_t LORA_LED_starttime = 0; +#endif enum PowerState { discharging, charging, charged, critical }; diff --git a/src/modules/Telemetry/DeviceTelemetry.cpp b/src/modules/Telemetry/DeviceTelemetry.cpp index 1c2d18c717c..8bd70494e4f 100644 --- a/src/modules/Telemetry/DeviceTelemetry.cpp +++ b/src/modules/Telemetry/DeviceTelemetry.cpp @@ -20,6 +20,7 @@ static constexpr uint16_t TX_HISTORY_KEY_DEVICE_TELEMETRY = 0x8001; int32_t DeviceTelemetryModule::runOnce() { + refreshUptime(); uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_DEVICE_TELEMETRY) : 0; bool isImpoliteRole = isSensorOrRouterRole(); @@ -125,6 +126,8 @@ meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry() telemetry.variant.local_stats.num_online_nodes = numOnlineNodes; telemetry.variant.local_stats.num_total_nodes = nodeDB->getNumMeshNodes(); if (RadioLibInterface::instance) { + RadioLibInterface::instance->updateNoiseFloor(); + telemetry.variant.local_stats.noise_floor = RadioLibInterface::instance->getAverageNoiseFloor(); telemetry.variant.local_stats.num_packets_tx = RadioLibInterface::instance->txGood; telemetry.variant.local_stats.num_packets_rx = RadioLibInterface::instance->rxGood + RadioLibInterface::instance->rxBad; telemetry.variant.local_stats.num_packets_rx_bad = RadioLibInterface::instance->rxBad; @@ -133,6 +136,8 @@ meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry() } #ifdef ARCH_PORTDUINO if (SimRadio::instance) { + if (!RadioLibInterface::instance) + telemetry.variant.local_stats.noise_floor = SimRadio::instance->getCurrentRSSI(); telemetry.variant.local_stats.num_packets_tx = SimRadio::instance->txGood; telemetry.variant.local_stats.num_packets_rx = SimRadio::instance->rxGood + SimRadio::instance->rxBad; telemetry.variant.local_stats.num_packets_rx_bad = SimRadio::instance->rxBad; @@ -148,10 +153,11 @@ meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry() telemetry.variant.local_stats.num_tx_relay_canceled = router->txRelayCanceled; } - LOG_INFO("Sending local stats: uptime=%i, channel_utilization=%f, air_util_tx=%f, num_online_nodes=%i, num_total_nodes=%i", + LOG_INFO("Sending local stats: uptime=%i, channel_utilization=%f, air_util_tx=%f, num_online_nodes=%i, num_total_nodes=%i, " + "noise_floor=%d", telemetry.variant.local_stats.uptime_seconds, telemetry.variant.local_stats.channel_utilization, telemetry.variant.local_stats.air_util_tx, telemetry.variant.local_stats.num_online_nodes, - telemetry.variant.local_stats.num_total_nodes); + telemetry.variant.local_stats.num_total_nodes, telemetry.variant.local_stats.noise_floor); LOG_INFO("num_packets_tx=%i, num_packets_rx=%i, num_packets_rx_bad=%i", telemetry.variant.local_stats.num_packets_tx, telemetry.variant.local_stats.num_packets_rx, telemetry.variant.local_stats.num_packets_rx_bad); @@ -194,4 +200,4 @@ bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) service->sendToMesh(p, RX_SRC_LOCAL, true); } return true; -} \ No newline at end of file +} diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index b7b6e04a988..415a7ee9d04 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -127,6 +127,10 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "Sensor/T1000xSensor.h" #endif +#if __has_include() +#include "Sensor/SPA06Sensor.h" +#endif + #ifdef SENSECAP_INDICATOR #include "Sensor/IndicatorSensor.h" #endif @@ -166,12 +170,15 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) // Not a real I2C device, uses UART addSensor(i2cScanner, ScanI2C::DeviceType::NONE); #endif - addSensor(i2cScanner, ScanI2C::DeviceType::RCWL9620); - addSensor(i2cScanner, ScanI2C::DeviceType::CGRADSENS); +#if HAS_SPA06 && __has_include() + addSensor(i2cScanner, ScanI2C::DeviceType::SPA06); +#endif #endif #endif #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL + addSensor(i2cScanner, ScanI2C::DeviceType::RCWL9620); + addSensor(i2cScanner, ScanI2C::DeviceType::CGRADSENS); #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::DFROBOT_LARK); #endif @@ -449,7 +456,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt bool isCooldownOver = (now - lastAlertTime > 60000); if (isOwnTelemetry && bannerMsg && isCooldownOver) { - LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg); + LOG_INFO("drawFrame: IAQ %d (own) - showing banner: %s", m.iaq, bannerMsg); screen->showSimpleBanner(bannerMsg, 3000); // Only buzz if IAQ is over 200 diff --git a/src/modules/Telemetry/Sensor/SPA06Sensor.cpp b/src/modules/Telemetry/Sensor/SPA06Sensor.cpp new file mode 100644 index 00000000000..052e3f8ebe1 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SPA06Sensor.cpp @@ -0,0 +1,60 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SPA06Sensor.h" +#include "TelemetrySensor.h" +#include + +SPA06Sensor::SPA06Sensor() + : TelemetrySensor(meshtastic_TelemetrySensorType_SPA06, "SPA06"), spa_temp(nullptr), spa_pressure(nullptr) +{ +} + +bool SPA06Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + status = spa06.begin(dev->address.address, bus); + if (!status) { + return status; + } + // Set moderate precision for faster sampling + spa06.setPressureOversampling(SPA06_003_OVERSAMPLE_8); // 8x oversampling + spa06.setTemperatureOversampling(SPA06_003_OVERSAMPLE_8); // 8x oversampling + + // Set measurement rate. 1 Hz is ample for telemetry (polled every tens of seconds) + // and draws far less power than 32 Hz, while staying in continuous mode so getMetrics() + // remains non-blocking (fresh data always available). + spa06.setPressureMeasureRate(SPA06_003_RATE_1); // 1 Hz + spa06.setTemperatureMeasureRate(SPA06_003_RATE_1); // 1 Hz + spa06.setMeasurementMode(SPA06_003_MEAS_CONTINUOUS_BOTH); + + spa_temp = spa06.getTemperatureSensor(); + spa_pressure = spa06.getPressureSensor(); + + initI2CSensor(); + return status; +} + +bool SPA06Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + if (!spa_temp || !spa_pressure) { + return false; + } + + sensors_event_t temp, press; + + if (!spa_temp->getEvent(&temp) || !spa_pressure->getEvent(&press)) { + LOG_DEBUG("SPA06 getEvents no data"); + return false; + } + + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; + measurement->variant.environment_metrics.temperature = temp.temperature; + measurement->variant.environment_metrics.barometric_pressure = press.pressure; + + return true; +} +#endif diff --git a/src/modules/Telemetry/Sensor/SPA06Sensor.h b/src/modules/Telemetry/Sensor/SPA06Sensor.h new file mode 100644 index 00000000000..01aadb5cfdf --- /dev/null +++ b/src/modules/Telemetry/Sensor/SPA06Sensor.h @@ -0,0 +1,22 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class SPA06Sensor : public TelemetrySensor +{ + private: + Adafruit_SPA06_003 spa06; + Adafruit_Sensor *spa_temp; + Adafruit_Sensor *spa_pressure; + + public: + SPA06Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +}; + +#endif diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 4db80ba183c..632727b9240 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -15,15 +15,6 @@ WaypointModule *waypointModule; -static inline float degToRad(float deg) -{ - return deg * PI / 180.0f; -} -static inline float radToDeg(float rad) -{ - return rad * 180.0f / PI; -} - ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) @@ -91,9 +82,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, // === Header === graphics::drawCommonHeader(display, x, y, titleStr); - - const int w = display->getWidth(); - const int h = display->getHeight(); + const int *textPos = graphics::getTextPositions(display); // Decode the waypoint const meshtastic_MeshPacket &mp = devicestate.rx_waypoint; @@ -108,71 +97,118 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); // Will contain distance information, passed as a field to drawColumns - char distStr[20]; + char distStr[20] = ""; // Get our node, to use our own position meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - // Dimensions / co-ordinates for the compass/circle - const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h); - const int16_t compassX = x + w - (compassDiam / 2) - 5; - const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) - ? y + h / 2 - : y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2; + // Match compass sizing/placement to favorite node screen logic. + const int w = display->getWidth(); + int16_t compassRadius = 8; + int16_t compassX = x + w - compassRadius - 8; + int16_t compassY = y + display->getHeight() / 2; + + if (SCREEN_WIDTH > SCREEN_HEIGHT) { + const int16_t topY = textPos[1]; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); + const int16_t usableHeight = bottomY - topY - 5; + compassRadius = usableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + compassX = x + SCREEN_WIDTH - compassRadius - 8; + compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + } else { + // Waypoint content uses rows 1..4, so place the compass below that block. + const int yBelowContent = textPos[4] + FONT_HEIGHT_SMALL + 2; + const int margin = 4; +#if defined(USE_EINK) + const int iconSize = (graphics::currentResolution == graphics::ScreenResolution::High) ? 16 : 8; + const int navBarHeight = iconSize + 6; +#else + const int navBarHeight = 0; +#endif + const int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; + if (availableHeight > 0) { + compassRadius = availableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) + compassRadius = (SCREEN_WIDTH - 16) / 2; + if (compassRadius < 8) + compassRadius = 8; + compassX = x + SCREEN_WIDTH / 2; + compassY = yBelowContent + availableHeight / 2; + } + } + const uint16_t compassDiam = compassRadius * 2; + + const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode)); + const char *statusLine1 = nullptr; + const char *statusLine2 = nullptr; - // If our node has a position: - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { + // Distance only needs our own position fix; compass/bearing additionally needs heading. + if (hasOwnPositionFix) { const meshtastic_PositionLite &op = ourNode->position; - float myHeading; - if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { - myHeading = 0; - } else { - if (screen->hasHeading()) - myHeading = degToRad(screen->getHeading()); - else - myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - } - graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2)); - - // Compass bearing to waypoint - float bearingToOther = - GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i)); - // If the top of the compass is a static north then bearingToOther can be drawn on the compass directly - // If the top of the compass is not a static north we need adjust bearingToOther based on heading - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) - bearingToOther -= myHeading; - graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); - - float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; - bearingToOtherDegrees = radToDeg(bearingToOtherDegrees); - - // Distance to Waypoint - float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + const float d = + GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + + // Always show distance once we have an own-position fix, even without heading. if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { float feet = d * METERS_TO_FEET; - snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°", - feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees); + snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft" : "%.1fmi", + feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET); } else { - snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000, - bearingToOtherDegrees); + snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm" : "%.1fkm", d < 2000 ? d : d / 1000); } - } - else { - display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); + float myHeading = 0.0f; + const bool hasHeading = + graphics::CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading); + if (hasHeading) { + // Draw compass circle + display->drawCircle(compassX, compassY, compassRadius); + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + + // Compass bearing to waypoint + float bearingToOther = + GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i)); + bearingToOther = graphics::CompassRenderer::adjustBearingForCompassMode(bearingToOther, myHeading); + graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); + + const float bearingToOtherDegrees = graphics::CompassRenderer::radiansToDegrees360(bearingToOther); + + // Distance to waypoint with relative bearing when heading is available. + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + float feet = d * METERS_TO_FEET; + snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°", + feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees); + } else { + snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000, + bearingToOtherDegrees); + } - // ? in the distance field - snprintf(distStr, sizeof(distStr), "? %s ?°", - (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km"); + } else { + statusLine1 = "No"; + statusLine2 = "Heading"; + } + } else { + // No own fix yet, so compass/bearing data would be misleading. + statusLine1 = "No"; + statusLine2 = "Fix"; } - // Draw compass circle - display->drawCircle(compassX, compassY, compassDiam / 2); + if (statusLine1) { + display->drawCircle(compassX, compassY, compassRadius); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(compassX, compassY - FONT_HEIGHT_SMALL, statusLine1); + display->drawString(compassX, compassY, statusLine2); + } display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here! - display->drawString(0, graphics::getTextPositions(display)[line++], lastStr); - display->drawString(0, graphics::getTextPositions(display)[line++], wp.name); - display->drawString(0, graphics::getTextPositions(display)[line++], wp.description); - display->drawString(0, graphics::getTextPositions(display)[line++], distStr); + display->drawString(0, textPos[line++], lastStr); + display->drawString(0, textPos[line++], wp.name); + display->drawString(0, textPos[line++], wp.description); + if (distStr[0]) + display->drawString(0, textPos[line++], distStr); } #endif diff --git a/src/motion/AccelerometerThread.h b/src/motion/AccelerometerThread.h index d2205fd2a3e..8854ccf436f 100644 --- a/src/motion/AccelerometerThread.h +++ b/src/motion/AccelerometerThread.h @@ -16,6 +16,7 @@ #include "BMM150Sensor.h" #include "BMX160Sensor.h" #include "ICM20948Sensor.h" +#include "ICM42607PSensor.h" #include "LIS3DHSensor.h" #include "LSM6DS3Sensor.h" #include "MPU6050Sensor.h" @@ -111,6 +112,9 @@ class AccelerometerThread : public concurrency::OSThread case ScanI2C::DeviceType::ICM20948: sensor = new ICM20948Sensor(device); break; + case ScanI2C::DeviceType::ICM42607P: + sensor = new ICM42607PSensor(device); + break; case ScanI2C::DeviceType::BMM150: sensor = new BMM150Sensor(device); break; diff --git a/src/motion/BMM150Sensor.cpp b/src/motion/BMM150Sensor.cpp index 4b3a1215c13..f48d20288b1 100644 --- a/src/motion/BMM150Sensor.cpp +++ b/src/motion/BMM150Sensor.cpp @@ -7,9 +7,6 @@ extern graphics::Screen *screen; #endif -// Flag when an interrupt has been detected -volatile static bool BMM150_IRQ = false; - BMM150Sensor::BMM150Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} bool BMM150Sensor::init() @@ -23,24 +20,7 @@ int32_t BMM150Sensor::runOnce() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN float heading = sensor->getCompassDegree(); - - switch (config.display.compass_orientation) { - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: - heading += 90; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: - heading += 180; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: - heading += 270; - break; - } + heading = applyCompassOrientation(heading); if (screen) screen->setHeading(heading); #endif @@ -90,4 +70,4 @@ bool BMM150Singleton::init(ScanI2C::FoundDevice device) return true; } -#endif \ No newline at end of file +#endif diff --git a/src/motion/BMX160Sensor.cpp b/src/motion/BMX160Sensor.cpp index 5888c20bec1..02303faa4ff 100644 --- a/src/motion/BMX160Sensor.cpp +++ b/src/motion/BMX160Sensor.cpp @@ -16,6 +16,7 @@ bool BMX160Sensor::init() if (sensor.begin()) { // set output data rate sensor.ODR_Config(BMX160_ACCEL_ODR_100HZ, BMX160_GYRO_ODR_100HZ); + loadMagnetometerCalibration(compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); LOG_DEBUG("BMX160 init ok"); return true; } @@ -33,42 +34,12 @@ int32_t BMX160Sensor::runOnce() sensor.getAllData(&magAccel, NULL, &gAccel); if (doCalibration) { - - if (!showingScreen) { - powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration - showingScreen = true; - if (screen) - screen->startAlert((FrameCallback)drawFrameCalibration); - } - - if (magAccel.x > highestX) - highestX = magAccel.x; - if (magAccel.x < lowestX) - lowestX = magAccel.x; - if (magAccel.y > highestY) - highestY = magAccel.y; - if (magAccel.y < lowestY) - lowestY = magAccel.y; - if (magAccel.z > highestZ) - highestZ = magAccel.z; - if (magAccel.z < lowestZ) - lowestZ = magAccel.z; - - uint32_t now = millis(); - if (now > endCalibrationAt) { - doCalibration = false; - endCalibrationAt = 0; - showingScreen = false; - if (screen) - screen->endAlert(); - } - - // LOG_DEBUG("BMX160 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, - // lowestY, highestY, lowestZ, highestZ); + beginCalibrationDisplay(showingScreen); + updateCalibrationExtrema(magAccel.x, magAccel.y, magAccel.z, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, + lowestZ); } - int highestRealX = highestX - (highestX + lowestX) / 2; - magAccel.x -= (highestX + lowestX) / 2; magAccel.y -= (highestY + lowestY) / 2; magAccel.z -= (highestZ + lowestZ) / 2; @@ -88,23 +59,7 @@ int32_t BMX160Sensor::runOnce() float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma); - switch (config.display.compass_orientation) { - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: - heading += 90; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: - heading += 180; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: - heading += 270; - break; - } + heading = applyCompassOrientation(heading); if (screen) screen->setHeading(heading); #endif @@ -119,15 +74,8 @@ void BMX160Sensor::calibrate(uint16_t forSeconds) sBmx160SensorData_t gAccel; LOG_DEBUG("BMX160 calibration started for %is", forSeconds); sensor.getAllData(&magAccel, NULL, &gAccel); - highestX = magAccel.x, lowestX = magAccel.x; - highestY = magAccel.y, lowestY = magAccel.y; - highestZ = magAccel.z, lowestZ = magAccel.z; - - doCalibration = true; - uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided - endCalibrationAt = millis() + calibrateFor; - if (screen) - screen->setEndCalibration(endCalibrationAt); + seedCalibrationExtrema(magAccel.x, magAccel.y, magAccel.z, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + startCalibrationWindow(forSeconds); #endif } diff --git a/src/motion/BMX160Sensor.h b/src/motion/BMX160Sensor.h index ddca5767c74..d60477521c9 100644 --- a/src/motion/BMX160Sensor.h +++ b/src/motion/BMX160Sensor.h @@ -17,6 +17,7 @@ class BMX160Sensor : public MotionSensor private: RAK_BMX160 sensor; bool showingScreen = false; + static constexpr const char *compassCalibrationFileName = "/prefs/compass_bmx160.dat"; float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; public: @@ -39,4 +40,4 @@ class BMX160Sensor : public MotionSensor #endif -#endif \ No newline at end of file +#endif diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp index ecada208575..e44994a6006 100644 --- a/src/motion/ICM20948Sensor.cpp +++ b/src/motion/ICM20948Sensor.cpp @@ -26,7 +26,11 @@ bool ICM20948Sensor::init() return false; // Enable simple Wake on Motion - return sensor->setWakeOnMotion(); + bool wakeOnMotionOk = sensor->setWakeOnMotion(); + if (wakeOnMotionOk) { + loadMagnetometerCalibration(compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + } + return wakeOnMotionOk; } #ifdef ICM_20948_INT_PIN @@ -47,7 +51,8 @@ int32_t ICM20948Sensor::runOnce() int32_t ICM20948Sensor::runOnce() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN - if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) { + if (screen && !doCalibration && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && + !config.device.double_tap_as_button_press) { if (!isAsleep) { LOG_DEBUG("sleeping IMU"); sensor->sleep(true); @@ -69,38 +74,10 @@ int32_t ICM20948Sensor::runOnce() } if (doCalibration) { - - if (!showingScreen) { - powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration - showingScreen = true; - if (screen) - screen->startAlert((FrameCallback)drawFrameCalibration); - } - - if (magX > highestX) - highestX = magX; - if (magX < lowestX) - lowestX = magX; - if (magY > highestY) - highestY = magY; - if (magY < lowestY) - lowestY = magY; - if (magZ > highestZ) - highestZ = magZ; - if (magZ < lowestZ) - lowestZ = magZ; - - uint32_t now = millis(); - if (now > endCalibrationAt) { - doCalibration = false; - endCalibrationAt = 0; - showingScreen = false; - if (screen) - screen->endAlert(); - } - - // LOG_DEBUG("ICM20948 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, - // lowestY, highestY, lowestZ, highestZ); + beginCalibrationDisplay(showingScreen); + updateCalibrationExtrema(magX, magY, magZ, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, + lowestZ); } magX -= (highestX + lowestX) / 2; @@ -122,23 +99,7 @@ int32_t ICM20948Sensor::runOnce() float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma); - switch (config.display.compass_orientation) { - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: - heading += 90; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: - heading += 180; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: - heading += 270; - break; - } + heading = applyCompassOrientation(heading); if (screen) screen->setHeading(heading); #endif @@ -169,26 +130,16 @@ int32_t ICM20948Sensor::runOnce() void ICM20948Sensor::calibrate(uint16_t forSeconds) { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN - LOG_DEBUG("Old calibration data: highestX = %f, lowestX = %f, highestY = %f, lowestY = %f, highestZ = %f, lowestZ = %f", - highestX, lowestX, highestY, lowestY, highestZ, lowestZ); - LOG_DEBUG("BMX160 calibration started for %is", forSeconds); + LOG_DEBUG("ICM20948 cal start %is", forSeconds); if (sensor->dataReady()) { sensor->getAGMT(); - highestX = sensor->agmt.mag.axes.x; - lowestX = sensor->agmt.mag.axes.x; - highestY = sensor->agmt.mag.axes.y; - lowestY = sensor->agmt.mag.axes.y; - highestZ = sensor->agmt.mag.axes.z; - lowestZ = sensor->agmt.mag.axes.z; + seedCalibrationExtrema(sensor->agmt.mag.axes.x, sensor->agmt.mag.axes.y, sensor->agmt.mag.axes.z, highestX, lowestX, + highestY, lowestY, highestZ, lowestZ); } else { - highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; + seedCalibrationExtrema(0.0f, 0.0f, 0.0f, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); } - doCalibration = true; - uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided - endCalibrationAt = millis() + calibrateFor; - if (screen) - screen->setEndCalibration(endCalibrationAt); + startCalibrationWindow(forSeconds); #endif } // ---------------------------------------------------------------------- @@ -314,11 +265,6 @@ bool ICM20948Singleton::setWakeOnMotion() status = intEnableWOM(true); LOG_DEBUG("ICM20948 init set intEnableWOM - %s", statusString()); return status == ICM_20948_Stat_Ok; - - // Clear any current interrupts - ICM20948_IRQ = false; - clearInterrupts(); - return true; } #endif diff --git a/src/motion/ICM20948Sensor.h b/src/motion/ICM20948Sensor.h index 091cb9a1e95..d8369b3ca16 100644 --- a/src/motion/ICM20948Sensor.h +++ b/src/motion/ICM20948Sensor.h @@ -83,6 +83,7 @@ class ICM20948Sensor : public MotionSensor ICM20948Singleton *sensor = nullptr; bool showingScreen = false; bool isAsleep = false; + static constexpr const char *compassCalibrationFileName = "/prefs/compass_icm20948.dat"; #ifdef MUZI_BASE float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000, lowestZ = 98.000000; @@ -103,4 +104,4 @@ class ICM20948Sensor : public MotionSensor #endif -#endif \ No newline at end of file +#endif diff --git a/src/motion/ICM42607PSensor.cpp b/src/motion/ICM42607PSensor.cpp new file mode 100644 index 00000000000..0bace05a8f4 --- /dev/null +++ b/src/motion/ICM42607PSensor.cpp @@ -0,0 +1,98 @@ +#include "ICM42607PSensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() + +#include "detect/ScanI2CTwoWire.h" +#include + +static constexpr uint16_t ICM42607P_ACCEL_ODR_HZ = 50; +static constexpr uint16_t ICM42607P_ACCEL_FSR_G = 2; +static constexpr float ICM42607P_COUNTS_PER_G = 32768.0f / ICM42607P_ACCEL_FSR_G; + +#ifdef ICM_42607P_INT_PIN +volatile static bool ICM42607P_IRQ = false; + +void ICM42607PSetInterrupt() +{ + ICM42607P_IRQ = true; +} +#endif + +ICM42607PSensor::ICM42607PSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) +{ + wire = ScanI2CTwoWire::fetchI2CBus(foundDevice.address); +} + +ICM42607PSensor::~ICM42607PSensor() = default; + +bool ICM42607PSensor::init() +{ + bool addressLsb = deviceAddress() == ICM42607P_ADDR_ALT; + + LOG_DEBUG("ICM-42607-P begin on addr 0x%02X (port=%d)", deviceAddress(), devicePort()); + sensor.reset(); + auto newSensor = std::make_unique(*wire, addressLsb); + + int status = newSensor->begin(); + // ICM42670P library returns -3 for ICM42607P because WHO_AM_I differs; the register map is compatible. + if (status != 0 && status != -3) { + LOG_DEBUG("ICM-42607-P init error %d", status); + return false; + } + + status = newSensor->startAccel(ICM42607P_ACCEL_ODR_HZ, ICM42607P_ACCEL_FSR_G); + if (status != 0) { + LOG_DEBUG("ICM-42607-P accel start error %d", status); + return false; + } + +#ifdef ICM_42607P_INT_PIN + ICM42607P_IRQ = false; + status = newSensor->startWakeOnMotion(ICM_42607P_INT_PIN, ICM42607PSetInterrupt); + if (status != 0) { + LOG_DEBUG("ICM-42607-P wake-on-motion start error %d", status); + return false; + } + LOG_DEBUG("ICM-42607-P wake-on-motion interrupt ok pin=%d", ICM_42607P_INT_PIN); +#endif + + sensor = std::move(newSensor); + LOG_DEBUG("ICM-42607-P init ok"); + return true; +} + +int32_t ICM42607PSensor::runOnce() +{ +#ifdef ICM_42607P_INT_PIN + if (ICM42607P_IRQ) { + ICM42607P_IRQ = false; + LOG_DEBUG("ICM-42607-P motion interrupt"); + wakeScreen(); + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; +#else + int16_t x = 0; + int16_t y = 0; + int16_t z = 0; + inv_imu_sensor_event_t event = {}; + + if (sensor == nullptr || sensor->getDataFromRegisters(event) != 0) { + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + + // getDataFromRegisters() fills accel[] but does not set sensor_mask in this library version. + if (event.accel[0] == 0 && event.accel[1] == 0 && event.accel[2] == 0) { + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + + x = event.accel[0]; + y = event.accel[1]; + z = event.accel[2]; + // LOG_DEBUG("ICM-42607-P accel read x=%.3fg y=%.3fg z=%.3fg", (float)x / ICM42607P_COUNTS_PER_G, + // (float)y / ICM42607P_COUNTS_PER_G, (float)z / ICM42607P_COUNTS_PER_G); + + return MOTION_SENSOR_CHECK_INTERVAL_MS; +#endif +} + +#endif diff --git a/src/motion/ICM42607PSensor.h b/src/motion/ICM42607PSensor.h new file mode 100644 index 00000000000..370fef27644 --- /dev/null +++ b/src/motion/ICM42607PSensor.h @@ -0,0 +1,28 @@ +#pragma once +#ifndef _ICM42607P_SENSOR_H_ +#define _ICM42607P_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() + +#include + +class ICM42670; + +class ICM42607PSensor : public MotionSensor +{ + private: + std::unique_ptr sensor; + TwoWire *wire = nullptr; + + public: + explicit ICM42607PSensor(ScanI2C::FoundDevice foundDevice); + ~ICM42607PSensor() override; + virtual bool init() override; + virtual int32_t runOnce() override; +}; + +#endif + +#endif diff --git a/src/motion/MMC5983MASensor.cpp b/src/motion/MMC5983MASensor.cpp new file mode 100644 index 00000000000..ba09f464865 --- /dev/null +++ b/src/motion/MMC5983MASensor.cpp @@ -0,0 +1,115 @@ +#include "MMC5983MASensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() + +#include "detect/ScanI2CTwoWire.h" + +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) +extern graphics::Screen *screen; +#endif + +static constexpr float MMC5983MA_ZERO_FIELD = 131072.0f; +static constexpr float MMC5983MA_COUNTS_PER_GAUSS = 16384.0f; +static constexpr uint16_t MMC5983MA_CONTINUOUS_FREQUENCY_HZ = 10; +static constexpr float MMC5983MA_HEADING_OFFSET_DEG = 180.0f; + +MMC5983MASensor::MMC5983MASensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} + +bool MMC5983MASensor::init() +{ + LOG_DEBUG("MMC5983MA begin on addr 0x%02X (port=%d)", device.address.address, device.address.port); + TwoWire *wire = ScanI2CTwoWire::fetchI2CBus(device.address); + + if (!sensor.begin(*wire)) { + LOG_DEBUG("MMC5983MA init error"); + return false; + } + + sensor.softReset(); + sensor.setFilterBandwidth(100); + sensor.performSetOperation(); + sensor.enableAutomaticSetReset(); + continuousMode = sensor.setContinuousModeFrequency(MMC5983MA_CONTINUOUS_FREQUENCY_HZ); + continuousMode &= sensor.enableContinuousMode(); + + if (!continuousMode) { + LOG_DEBUG("MMC5983MA continuous mode failed, using single-shot reads"); + sensor.disableContinuousMode(); + } + + loadMagnetometerCalibration(compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + return true; +} + +bool MMC5983MASensor::readMagnetometer(float &xGauss, float &yGauss, float &zGauss) +{ + uint32_t rawX = 0; + uint32_t rawY = 0; + uint32_t rawZ = 0; + + if (!(continuousMode ? sensor.readFieldsXYZ(&rawX, &rawY, &rawZ) : sensor.getMeasurementXYZ(&rawX, &rawY, &rawZ))) { + LOG_DEBUG("MMC5983MA read failed"); + return false; + } + + xGauss = ((float)rawX - MMC5983MA_ZERO_FIELD) / MMC5983MA_COUNTS_PER_GAUSS; + yGauss = ((float)rawY - MMC5983MA_ZERO_FIELD) / MMC5983MA_COUNTS_PER_GAUSS; + zGauss = ((float)rawZ - MMC5983MA_ZERO_FIELD) / MMC5983MA_COUNTS_PER_GAUSS; + return true; +} + +int32_t MMC5983MASensor::runOnce() +{ + float magX = 0, magY = 0, magZ = 0; + if (!readMagnetometer(magX, magY, magZ)) { + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) + if (doCalibration) { + beginCalibrationDisplay(showingScreen); + updateCalibrationExtrema(magX, magY, magZ, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, + lowestZ); + } +#endif + + magX -= (highestX + lowestX) / 2; + magY -= (highestY + lowestY) / 2; + magZ -= (highestZ + lowestZ) / 2; + +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN + float heading = atan2f(magY, magX) * RAD_TO_DEG + MMC5983MA_HEADING_OFFSET_DEG; + if (heading < 0.0f) { + heading += 360.0f; + } else if (heading >= 360.0f) { + heading -= 360.0f; + } + + heading = applyCompassOrientation(heading); + if (screen) { + screen->setHeading(heading); + } +#endif + + return MOTION_SENSOR_CHECK_INTERVAL_MS; +} + +void MMC5983MASensor::calibrate(uint16_t forSeconds) +{ +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) + float xGauss = 0.0f; + float yGauss = 0.0f; + float zGauss = 0.0f; + + LOG_DEBUG("MMC5983MA calibration started for %is", forSeconds); + if (readMagnetometer(xGauss, yGauss, zGauss)) { + seedCalibrationExtrema(xGauss, yGauss, zGauss, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + } else { + seedCalibrationExtrema(0.0f, 0.0f, 0.0f, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + } + startCalibrationWindow(forSeconds); +#endif +} + +#endif diff --git a/src/motion/MMC5983MASensor.h b/src/motion/MMC5983MASensor.h new file mode 100644 index 00000000000..9d349b53a40 --- /dev/null +++ b/src/motion/MMC5983MASensor.h @@ -0,0 +1,31 @@ +#pragma once +#ifndef _MMC5983MA_SENSOR_H_ +#define _MMC5983MA_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() + +#include + +class MMC5983MASensor : public MotionSensor +{ + private: + SFE_MMC5983MA sensor; + bool continuousMode = false; + bool showingScreen = false; + static constexpr const char *compassCalibrationFileName = "/prefs/compass_mmc5983ma.dat"; + float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; + + bool readMagnetometer(float &xGauss, float &yGauss, float &zGauss); + + public: + explicit MMC5983MASensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; + virtual void calibrate(uint16_t forSeconds) override; +}; + +#endif + +#endif diff --git a/src/motion/MagnetometerThread.h b/src/motion/MagnetometerThread.h new file mode 100644 index 00000000000..affd99e948c --- /dev/null +++ b/src/motion/MagnetometerThread.h @@ -0,0 +1,115 @@ +#pragma once +#ifndef _MAGNETOMETER_THREAD_H_ +#define _MAGNETOMETER_THREAD_H_ + +#include "configuration.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER + +#include "../concurrency/OSThread.h" +#include "MMC5983MASensor.h" +#include "MotionSensor.h" + +extern ScanI2C::DeviceAddress magnetometer_found; + +class MagnetometerThread : public concurrency::OSThread +{ + private: + MotionSensor *sensor = nullptr; + ScanI2C::FoundDevice device; + bool isInitialised = false; + + public: + explicit MagnetometerThread(ScanI2C::FoundDevice foundDevice) : OSThread("Magnetometer") + { + device = foundDevice; + init(); + } + + explicit MagnetometerThread(ScanI2C::DeviceType type) : MagnetometerThread(ScanI2C::FoundDevice{type, magnetometer_found}) {} + + void start() + { + init(); + setIntervalFromNow(0); + }; + + void calibrate(uint16_t forSeconds) + { + if (sensor) { + sensor->calibrate(forSeconds); + setIntervalFromNow(0); + } + } + + protected: + int32_t runOnce() override + { + canSleep = true; + + if (isInitialised) { + return sensor->runOnce(); + } + + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + + private: + void init() + { + if (isInitialised) { + return; + } + + if (device.address.port == ScanI2C::I2CPort::NO_I2C || device.address.address == 0 || device.type == ScanI2C::NONE) { + LOG_DEBUG("MagnetometerThread Disable due to no sensors found"); + disable(); + return; + } + + switch (device.type) { + case ScanI2C::DeviceType::MMC5983MA: + sensor = new MMC5983MASensor(device); + break; + default: + disable(); + return; + } + + isInitialised = sensor->init(); + if (!isInitialised) { + clean(); + } + LOG_DEBUG("MagnetometerThread::init %s", isInitialised ? "ok" : "failed"); + } + + MagnetometerThread(const MagnetometerThread &other) : OSThread::OSThread("Magnetometer") { this->copy(other); } + + virtual ~MagnetometerThread() { clean(); } + + MagnetometerThread &operator=(const MagnetometerThread &other) + { + this->copy(other); + return *this; + } + + void copy(const MagnetometerThread &other) + { + if (this != &other) { + clean(); + this->device = ScanI2C::FoundDevice(other.device.type, + ScanI2C::DeviceAddress(other.device.address.port, other.device.address.address)); + } + } + + void clean() + { + isInitialised = false; + delete sensor; + sensor = nullptr; + } +}; + +#endif + +#endif diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp index d0bfe4e2ce3..83231aea90c 100644 --- a/src/motion/MotionSensor.cpp +++ b/src/motion/MotionSensor.cpp @@ -1,10 +1,37 @@ #include "MotionSensor.h" +#include "FSCommon.h" +#include "SPILock.h" +#include "SafeFile.h" #include "graphics/draw/CompassRenderer.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C char timeRemainingBuffer[12]; +namespace +{ +constexpr uint32_t COMPASS_CALIBRATION_MAGIC = 0x4D43414CL; // "MCAL" +constexpr uint16_t COMPASS_CALIBRATION_VERSION = 1; + +struct CompassCalibrationRecord { + uint32_t magic; + uint16_t version; + uint16_t reserved; + float highestX; + float lowestX; + float highestY; + float lowestY; + float highestZ; + float lowestZ; +}; + +bool isRangeValid(float highest, float lowest) +{ + // NaN/Inf guard without pulling in extra math helpers. + return (highest == highest) && (lowest == lowest) && (highest > lowest); +} +} // namespace + // screen is defined in main.cpp extern graphics::Screen *screen; @@ -32,33 +59,237 @@ ScanI2C::I2CPort MotionSensor::devicePort() return device.address.port; } +bool MotionSensor::saveMagnetometerCalibration(const char *filePath, float highestX, float lowestX, float highestY, float lowestY, + float highestZ, float lowestZ) +{ +#ifdef FSCom + if (!isRangeValid(highestX, lowestX) || !isRangeValid(highestY, lowestY) || !isRangeValid(highestZ, lowestZ)) { + return false; + } + + FSCom.mkdir("/prefs"); + CompassCalibrationRecord record = { + COMPASS_CALIBRATION_MAGIC, COMPASS_CALIBRATION_VERSION, 0, highestX, lowestX, highestY, lowestY, highestZ, lowestZ}; + + auto file = SafeFile(filePath, true); + const size_t written = file.write(reinterpret_cast(&record), sizeof(record)); + return (written == sizeof(record)) && file.close(); +#else + return false; +#endif +} + +bool MotionSensor::loadMagnetometerCalibration(const char *filePath, float &highestX, float &lowestX, float &highestY, + float &lowestY, float &highestZ, float &lowestZ) +{ +#ifdef FSCom + CompassCalibrationRecord record = {}; + size_t bytesRead = 0; + + spiLock->lock(); + auto file = FSCom.open(filePath, FILE_O_READ); + if (!file) { + spiLock->unlock(); + return false; + } + bytesRead = file.read(reinterpret_cast(&record), sizeof(record)); + file.close(); + spiLock->unlock(); + + const bool headerValid = (bytesRead == sizeof(record)) && (record.magic == COMPASS_CALIBRATION_MAGIC) && + (record.version == COMPASS_CALIBRATION_VERSION) && (record.reserved == 0U); + const bool rangeValid = isRangeValid(record.highestX, record.lowestX) && isRangeValid(record.highestY, record.lowestY) && + isRangeValid(record.highestZ, record.lowestZ); + if (!headerValid || !rangeValid) { + return false; + } + + highestX = record.highestX; + lowestX = record.lowestX; + highestY = record.highestY; + lowestY = record.lowestY; + highestZ = record.highestZ; + lowestZ = record.lowestZ; + + return true; +#else + return false; +#endif +} + +void MotionSensor::beginCalibrationDisplay(bool &showingScreen) +{ +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN + if (!showingScreen) { + powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration + showingScreen = true; + if (screen) + screen->startAlert((FrameCallback)drawFrameCalibration); + } +#else + (void)showingScreen; +#endif +} + +void MotionSensor::finishCalibrationIfExpired(bool &showingScreen, const char *filePath, float highestX, float lowestX, + float highestY, float lowestY, float highestZ, float lowestZ) +{ + const uint32_t now = millis(); + if ((int32_t)(now - endCalibrationAt) < 0) + return; + + doCalibration = false; + endCalibrationAt = 0; + showingScreen = false; + saveMagnetometerCalibration(filePath, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN + if (screen) { + screen->setEndCalibration(0); + screen->endAlert(); + } +#endif +} + +void MotionSensor::startCalibrationWindow(uint16_t forSeconds) +{ + doCalibration = true; + const uint32_t calibrateFor = static_cast(forSeconds) * 1000U; + endCalibrationAt = millis() + calibrateFor; +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN + if (screen) + screen->setEndCalibration(endCalibrationAt); +#endif +} + +void MotionSensor::seedCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY, + float &lowestY, float &highestZ, float &lowestZ) +{ + highestX = lowestX = x; + highestY = lowestY = y; + highestZ = lowestZ = z; +} + +void MotionSensor::updateCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY, + float &lowestY, float &highestZ, float &lowestZ) +{ + if (x > highestX) + highestX = x; + if (x < lowestX) + lowestX = x; + if (y > highestY) + highestY = y; + if (y < lowestY) + lowestY = y; + if (z > highestZ) + highestZ = z; + if (z < lowestZ) + lowestZ = z; +} + +float MotionSensor::applyCompassOrientation(float heading) +{ + switch (config.display.compass_orientation) { + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: + return heading + 90; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: + return heading + 180; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: + return heading + 270; + default: + return heading; + } +} + #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { if (screen == nullptr) return; - // int x_offset = display->width() / 2; - // int y_offset = display->height() <= 80 ? 0 : 32; - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_MEDIUM); - display->drawString(x, y, "Calibrating\nCompass"); - uint8_t timeRemaining = (screen->getEndCalibration() - millis()) / 1000; - sprintf(timeRemainingBuffer, "( %02d )", timeRemaining); - display->setFont(FONT_SMALL); - display->drawString(x, y + 40, timeRemainingBuffer); + const int16_t width = display->getWidth(); + const int16_t height = display->getHeight(); + const bool compactLayout = (height <= 80); + const int16_t margin = 4; + + const uint32_t now = millis(); + const uint32_t endCalibrationAt = screen->getEndCalibration(); + uint32_t timeRemaining = 0; + if (endCalibrationAt > now) { + timeRemaining = (endCalibrationAt - now + 999) / 1000; + } int16_t compassX = 0, compassY = 0; - uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight()); + uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(width, height); + const int16_t compassRadius = compassDiam / 2; // coordinates for the center of the compass/circle if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - compassX = x + display->getWidth() - compassDiam / 2 - 5; - compassY = y + display->getHeight() / 2; + compassX = x + width - compassRadius - margin; + compassY = y + height / 2; + } else { + compassX = x + width - compassRadius - margin; + compassY = y + FONT_HEIGHT_SMALL + (height - FONT_HEIGHT_SMALL) / 2; + } + + const int16_t textLeft = x + 1; + const int16_t textRight = compassX - compassRadius - margin; + const int16_t textWidth = textRight - textLeft; + int16_t lineY = y; + + display->setTextAlignment(TEXT_ALIGN_LEFT); + if (textWidth > 12) { + const char *title = "Cal"; + const char *line1 = "Figure-8"; + const char *line2 = "Rotate axes"; + const char *line3 = "Away from metal"; + + display->setFont(FONT_SMALL); + if (!compactLayout && display->getStringWidth("Compass Calibration") <= textWidth) { + display->setFont(FONT_MEDIUM); + title = "Compass Calibration"; + line1 = "Move in figure-8"; + line2 = "Rotate all axes"; + line3 = "Keep from metal"; + display->drawString(textLeft, lineY, title); + lineY += FONT_HEIGHT_MEDIUM; + display->setFont(FONT_SMALL); + } else if (display->getStringWidth("Compass Cal") <= textWidth) { + title = "Compass Cal"; + if (textWidth >= display->getStringWidth("Move in figure-8")) { + line1 = "Move in figure-8"; + line2 = "Rotate all axes"; + line3 = "Keep from metal"; + } + display->drawString(textLeft, lineY, title); + lineY += FONT_HEIGHT_SMALL; + } else { + display->drawString(textLeft, lineY, title); + lineY += FONT_HEIGHT_SMALL; + } + + display->drawString(textLeft, lineY, line1); + lineY += FONT_HEIGHT_SMALL; + display->drawString(textLeft, lineY, line2); + lineY += FONT_HEIGHT_SMALL; + if (!compactLayout || textWidth >= display->getStringWidth(line3)) { + display->drawString(textLeft, lineY, line3); + } + } + + if (textWidth >= display->getStringWidth("000s left")) { + snprintf(timeRemainingBuffer, sizeof(timeRemainingBuffer), "%lus left", (unsigned long)timeRemaining); } else { - compassX = x + display->getWidth() - compassDiam / 2 - 5; - compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2; + snprintf(timeRemainingBuffer, sizeof(timeRemainingBuffer), "%lus", (unsigned long)timeRemaining); + } + display->setFont(FONT_SMALL); + if (textWidth > 12) { + display->drawString(textLeft, y + height - FONT_HEIGHT_SMALL - 1, timeRemainingBuffer); } + display->drawCircle(compassX, compassY, compassDiam / 2); graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180, (compassDiam / 2)); } diff --git a/src/motion/MotionSensor.h b/src/motion/MotionSensor.h index 8eb3bf95b88..71b71f73ac0 100644 --- a/src/motion/MotionSensor.h +++ b/src/motion/MotionSensor.h @@ -2,7 +2,7 @@ #ifndef _MOTION_SENSOR_H_ #define _MOTION_SENSOR_H_ -#define MOTION_SENSOR_CHECK_INTERVAL_MS 100 +#define MOTION_SENSOR_CHECK_INTERVAL_MS 50 #define MOTION_SENSOR_CLICK_THRESHOLD 40 #include "../configuration.h" @@ -54,6 +54,20 @@ class MotionSensor static void drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #endif + bool saveMagnetometerCalibration(const char *filePath, float highestX, float lowestX, float highestY, float lowestY, + float highestZ, float lowestZ); + bool loadMagnetometerCalibration(const char *filePath, float &highestX, float &lowestX, float &highestY, float &lowestY, + float &highestZ, float &lowestZ); + void beginCalibrationDisplay(bool &showingScreen); + void finishCalibrationIfExpired(bool &showingScreen, const char *filePath, float highestX, float lowestX, float highestY, + float lowestY, float highestZ, float lowestZ); + void startCalibrationWindow(uint16_t forSeconds); + static void seedCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY, + float &lowestY, float &highestZ, float &lowestZ); + static void updateCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY, + float &lowestY, float &highestZ, float &lowestZ); + static float applyCompassOrientation(float heading); + ScanI2C::FoundDevice device; // Do calibration if true @@ -63,4 +77,4 @@ class MotionSensor #endif -#endif \ No newline at end of file +#endif diff --git a/src/platform/esp32/ExtensionIOMCP23017.cpp b/src/platform/esp32/ExtensionIOMCP23017.cpp new file mode 100644 index 00000000000..48c1058c982 --- /dev/null +++ b/src/platform/esp32/ExtensionIOMCP23017.cpp @@ -0,0 +1,192 @@ +#include "configuration.h" + +#if defined(ARCH_ESP32) && defined(USE_MCP23017) + +#include "ExtensionIOMCP23017.h" +#include "concurrency/LockGuard.h" + +ExtensionIOMCP23017 mcpIoExpander; + +void ExtensionIOMCP23017::begin(TwoWire &wire, uint8_t addr, int sda, int scl) +{ + if (_begun) + return; + _wire = &wire; + _addr = addr; + wire.begin(sda, scl); + _begun = true; +} + +bool ExtensionIOMCP23017::readReg(uint8_t reg, uint8_t &val) +{ + if (!_wire || !_begun) + return false; + _wire->beginTransmission(_addr); + _wire->write(reg); + if (_wire->endTransmission() != 0) + return false; + if (_wire->requestFrom((int)_addr, 1) != 1 || !_wire->available()) + return false; + val = (uint8_t)_wire->read(); + return true; +} + +uint8_t ExtensionIOMCP23017::readReg(uint8_t reg) +{ + uint8_t val = 0; + readReg(reg, val); + return val; +} + +void ExtensionIOMCP23017::writeReg(uint8_t reg, uint8_t val) +{ + if (!_wire || !_begun) + return; + _wire->beginTransmission(_addr); + _wire->write(reg); + _wire->write(val); + _wire->endTransmission(); +} + +void ExtensionIOMCP23017::updateDirectionBit(uint8_t pin, bool asOutput) +{ + uint8_t reg = iodirRegForPin(pin); + uint8_t v; + if (!readReg(reg, v)) // skip the write rather than clobber the other 7 pins on a failed read + return; + uint8_t mask = bitForPin(pin); + if (asOutput) + v &= ~mask; // output: bit 0 + else + v |= mask; // input: bit 1 + writeReg(reg, v); +} + +void ExtensionIOMCP23017::pinMode(uint8_t pin, uint8_t mode) +{ + if (pin > 15) + return; + concurrency::LockGuard guard(&_lock); + if (mode == OUTPUT) { + updateDirectionBit(pin, true); + } else { + updateDirectionBit(pin, false); + uint8_t reg = gppuRegForPin(pin); + uint8_t v; + if (!readReg(reg, v)) // skip rather than clobber the bank on a failed read + return; + if (mode == INPUT_PULLUP) + v |= bitForPin(pin); + else + v &= ~bitForPin(pin); + writeReg(reg, v); + } +} + +void ExtensionIOMCP23017::digitalWrite(uint8_t pin, uint8_t value) +{ + if (pin > 15) + return; + concurrency::LockGuard guard(&_lock); + uint8_t reg = olatRegForPin(pin); + uint8_t v; + if (!readReg(reg, v)) // skip rather than clobber the bank (e.g. drop LORA_NRST) on a failed read + return; + uint8_t mask = bitForPin(pin); + if (value == LOW) + v &= ~mask; + else + v |= mask; + writeReg(reg, v); +} + +int ExtensionIOMCP23017::digitalRead(uint8_t pin) +{ + if (pin > 15) + return LOW; + concurrency::LockGuard guard(&_lock); + uint8_t v; + if (!readReg(gpioRegForPin(pin), v)) + return HIGH; // fail-safe: a failed read must not look like "LoRa BUSY released" and let RadioLib + // start an SPI transaction too early. DIO1 is polled via the radio IRQ register, not + // this pin, so reading it high on error is harmless there. + return (v & bitForPin(pin)) ? HIGH : LOW; +} + +void ExtensionIOMCP23017::enablePinChangeInterrupt(uint8_t pin, bool enable) +{ + if (pin > 15) + return; + concurrency::LockGuard guard(&_lock); + uint8_t regG = gpintenRegForPin(pin); + uint8_t v; + if (!readReg(regG, v)) // skip rather than clobber the bank on a failed read + return; + if (enable) + v |= bitForPin(pin); + else + v &= ~bitForPin(pin); + writeReg(regG, v); + // INTCON: 0 = interrupt on pin change from previous value + uint8_t regI = intconRegForPin(pin); + if (!readReg(regI, v)) + return; + v &= ~bitForPin(pin); + writeReg(regI, v); +} + +void ExtensionIOMCP23017::clearInterruptLatches() +{ + if (!_begun) + return; + concurrency::LockGuard guard(&_lock); + (void)readReg(0x10); // INTCAPA - clears INTA condition + (void)readReg(0x11); // INTCAPB +} + +uint8_t ExtensionIOMCP23017::readRegister(uint8_t reg) +{ + concurrency::LockGuard guard(&_lock); + return readReg(reg); +} + +void mcp23017EarlyInit() +{ + mcpIoExpander.begin(Wire, MCP23017_ADDR, I2C_SDA, I2C_SCL); + +#ifdef EXIO_LCD_RST + mcpIoExpander.pinMode(EXIO_LCD_RST, OUTPUT); + mcpIoExpander.digitalWrite(EXIO_LCD_RST, LOW); + delay(10); + mcpIoExpander.digitalWrite(EXIO_LCD_RST, HIGH); + delay(20); +#endif + +#ifdef EXIO_LORA_NRST + mcpIoExpander.pinMode(EXIO_LORA_NRST, OUTPUT); + mcpIoExpander.digitalWrite(EXIO_LORA_NRST, HIGH); +#endif + +#ifdef EXIO_LORA_BUSY + mcpIoExpander.pinMode(EXIO_LORA_BUSY, INPUT); +#endif + +#ifdef EXIO_LORA_DIO1 + mcpIoExpander.pinMode(EXIO_LORA_DIO1, INPUT); +#endif + +#ifdef EXIO_GPS_WAKE + mcpIoExpander.pinMode(EXIO_GPS_WAKE, OUTPUT); + mcpIoExpander.digitalWrite(EXIO_GPS_WAKE, HIGH); +#endif + +#ifdef EXIO_IMU_INT1 + mcpIoExpander.pinMode(EXIO_IMU_INT1, INPUT); +#endif + + LOG_INFO("MCP23017 0x%02x: IODIRA=0x%02x IODIRB=0x%02x GPIOA=0x%02x GPIOB=0x%02x", MCP23017_ADDR, + mcpIoExpander.readRegister(0x00), mcpIoExpander.readRegister(0x01), mcpIoExpander.readRegister(0x12), + mcpIoExpander.readRegister(0x13)); +} + +#endif // ARCH_ESP32 && USE_MCP23017 diff --git a/src/platform/esp32/ExtensionIOMCP23017.h b/src/platform/esp32/ExtensionIOMCP23017.h new file mode 100644 index 00000000000..f540019c74d --- /dev/null +++ b/src/platform/esp32/ExtensionIOMCP23017.h @@ -0,0 +1,66 @@ +#pragma once + +#include "concurrency/Lock.h" +#include +#include + +/** + * MCP23017 16-bit I2C GPIO expander (pins 0-15 = GPA0-7, GPB0-7). + * + * Used by boards that route radio/display control lines through the expander + * (e.g. Meshnology W10). RadioLib access goes through MCP23017LockingArduinoHal, + * which maps virtual pins MCP23017_VPIN_BASE..+15 onto local pins 0-15. + */ +class ExtensionIOMCP23017 +{ + public: + ExtensionIOMCP23017() : _wire(nullptr), _addr(0), _begun(false) {} + + void begin(TwoWire &wire, uint8_t addr, int sda, int scl); + + /** Local pin index 0-15 (GPA0=0 ... GPB7=15), not the virtual RadioLib pin. */ + void pinMode(uint8_t pin, uint8_t mode); + void digitalWrite(uint8_t pin, uint8_t value); + int digitalRead(uint8_t pin); + + /** Enable the MCP23017 pin-change interrupt so /INT asserts (only useful if /INT is wired to the MCU). */ + void enablePinChangeInterrupt(uint8_t pin, bool enable); + + /** Read INTCAPx to clear a latched interrupt condition after MCP /INT asserts. */ + void clearInterruptLatches(); + + /** Raw register read (0x00-0x1A), for bring-up / debug. */ + uint8_t readRegister(uint8_t reg); + + private: + uint8_t readReg(uint8_t reg); + // Checked read: returns false (and leaves val untouched) on any I2C error, so a glitched read + // can't drive a read-modify-write that clobbers the rest of the bank. + bool readReg(uint8_t reg, uint8_t &val); + void writeReg(uint8_t reg, uint8_t val); + void updateDirectionBit(uint8_t pin, bool asOutput); + uint8_t iodirRegForPin(uint8_t pin) const { return pin < 8 ? 0x00 : 0x01; } + uint8_t gpioRegForPin(uint8_t pin) const { return pin < 8 ? 0x12 : 0x13; } + uint8_t olatRegForPin(uint8_t pin) const { return pin < 8 ? 0x14 : 0x15; } + uint8_t gppuRegForPin(uint8_t pin) const { return pin < 8 ? 0x0C : 0x0D; } + uint8_t gpintenRegForPin(uint8_t pin) const { return pin < 8 ? 0x04 : 0x05; } + uint8_t intconRegForPin(uint8_t pin) const { return pin < 8 ? 0x08 : 0x09; } + uint8_t bitForPin(uint8_t pin) const { return 1u << (pin & 7); } + + TwoWire *_wire; + uint8_t _addr; + bool _begun; + // Serializes register access: the radio HAL (BUSY/DIO1/RESET) and AudioThread (amp enable) both + // reach this expander from different threads, and the read-modify-write paths are not atomic. + concurrency::Lock _lock; +}; + +/** Global instance shared by the early-init hook and the RadioLib HAL. */ +extern ExtensionIOMCP23017 mcpIoExpander; + +/** + * Bring up the expander and set board-specific pin directions/levels (LoRa reset, + * LCD reset, GPS wake, ...). Must run after power->setup() so the PMU rails are up, + * and before the I2C scan / radio / display init. + */ +void mcp23017EarlyInit(); diff --git a/src/platform/esp32/MCP23017LockingArduinoHal.cpp b/src/platform/esp32/MCP23017LockingArduinoHal.cpp new file mode 100644 index 00000000000..3182b9ae3a8 --- /dev/null +++ b/src/platform/esp32/MCP23017LockingArduinoHal.cpp @@ -0,0 +1,107 @@ +#include "configuration.h" + +#if defined(ARCH_ESP32) && defined(USE_MCP23017) + +#include "MCP23017LockingArduinoHal.h" +#include "SPILock.h" + +MCP23017LockingArduinoHal::MCP23017LockingArduinoHal(SPIClass &spi, SPISettings spiSettings, ExtensionIOMCP23017 &expander) + : LockingArduinoHal(spi, spiSettings), mcp(expander) +{ +#if MCP23017_INT_ESP32_PIN < 0 +#if defined(LORA_DIO1_SOFTWARE_POLL) + LOG_INFO("MCP23017 /INT not wired: LoRa DIO1 IRQ simulated by polling the radio IRQ status register"); +#else + LOG_WARN("MCP23017_INT_ESP32_PIN unset and no LORA_DIO1_SOFTWARE_POLL: LoRa DIO1 interrupts will not work"); +#endif +#endif +} + +bool MCP23017LockingArduinoHal::isMcpPin(uint32_t pin) +{ + return pin >= MCP23017_VPIN_BASE && pin <= MCP23017_VPIN_BASE + 15; +} + +void MCP23017LockingArduinoHal::pinMode(uint32_t pin, uint32_t mode) +{ + if (isMcpPin(pin)) { + uint8_t local = (uint8_t)(pin - MCP23017_VPIN_BASE); + mcp.pinMode(local, mode == GpioModeOutput ? OUTPUT : INPUT); + return; + } + ArduinoHal::pinMode(pin, mode); +} + +void MCP23017LockingArduinoHal::digitalWrite(uint32_t pin, uint32_t value) +{ + if (isMcpPin(pin)) { + uint8_t local = (uint8_t)(pin - MCP23017_VPIN_BASE); + mcp.digitalWrite(local, value == GpioLevelHigh ? HIGH : LOW); + return; + } + ArduinoHal::digitalWrite(pin, value); +} + +uint32_t MCP23017LockingArduinoHal::digitalRead(uint32_t pin) +{ + if (isMcpPin(pin)) { + uint8_t local = (uint8_t)(pin - MCP23017_VPIN_BASE); + return (uint32_t)mcp.digitalRead(local); + } + return ArduinoHal::digitalRead(pin); +} + +void MCP23017LockingArduinoHal::attachInterrupt(uint32_t interruptNum, void (*cb)(void), uint32_t mode) +{ +#if MCP23017_INT_ESP32_PIN >= 0 + uint8_t idx = (uint8_t)(SX126X_DIO1 - MCP23017_VPIN_BASE); + if (idx <= 15) + mcp.enablePinChangeInterrupt(idx, true); + // MCP23017 /INT is open-drain active-low: trigger on the falling edge of the INT line + // (RadioLib passes the DIO rising-edge mode, which applies to the DIO pin, not /INT). + ArduinoHal::attachInterrupt(interruptNum, cb, GpioInterruptFalling); +#else + ArduinoHal::attachInterrupt(interruptNum, cb, mode); +#endif +} + +void MCP23017LockingArduinoHal::detachInterrupt(uint32_t interruptNum) +{ + ArduinoHal::detachInterrupt(interruptNum); +#if MCP23017_INT_ESP32_PIN >= 0 + uint8_t idx = (uint8_t)(SX126X_DIO1 - MCP23017_VPIN_BASE); + if (idx <= 15) + mcp.enablePinChangeInterrupt(idx, false); +#endif +} + +uint32_t MCP23017LockingArduinoHal::pinToInterrupt(uint32_t pin) +{ + if (isMcpPin(pin)) { +#if MCP23017_INT_ESP32_PIN >= 0 + return ::digitalPinToInterrupt((unsigned int)MCP23017_INT_ESP32_PIN); +#else + return RADIOLIB_NC; +#endif + } + return ArduinoHal::pinToInterrupt(pin); +} + +#if MCP23017_INT_ESP32_PIN >= 0 +void MCP23017LockingArduinoHal::spiBeginTransaction() +{ + spiLock->lock(); + // Clear any latched expander interrupt before talking to the radio, so a stale /INT + // level doesn't mask the next DIO1 edge. + mcp.clearInterruptLatches(); + ArduinoHal::spiBeginTransaction(); +} + +void MCP23017LockingArduinoHal::spiEndTransaction() +{ + ArduinoHal::spiEndTransaction(); + spiLock->unlock(); +} +#endif + +#endif // ARCH_ESP32 && USE_MCP23017 diff --git a/src/platform/esp32/MCP23017LockingArduinoHal.h b/src/platform/esp32/MCP23017LockingArduinoHal.h new file mode 100644 index 00000000000..87984bc038e --- /dev/null +++ b/src/platform/esp32/MCP23017LockingArduinoHal.h @@ -0,0 +1,48 @@ +#pragma once + +#include "configuration.h" + +#if defined(ARCH_ESP32) && defined(USE_MCP23017) + +#include "mesh/RadioLibInterface.h" +#include "platform/esp32/ExtensionIOMCP23017.h" +#include + +#ifndef MCP23017_VPIN_BASE +#define MCP23017_VPIN_BASE 100 +#endif + +#ifndef MCP23017_INT_ESP32_PIN +#define MCP23017_INT_ESP32_PIN (-1) +#endif + +/** + * Routes RadioLib virtual GPIO MCP23017_VPIN_BASE..+15 to an MCP23017 I2C expander (GPA0-GPB7); + * all other pins fall through to the regular Arduino GPIO HAL. + * + * DIO1 interrupts: if the expander /INT line is wired to an ESP32 GPIO, set MCP23017_INT_ESP32_PIN + * and a real edge interrupt is used. If not (MCP23017_INT_ESP32_PIN < 0), define + * LORA_DIO1_SOFTWARE_POLL so the radio thread polls the radio's IRQ status register instead. + */ +class MCP23017LockingArduinoHal : public LockingArduinoHal +{ + public: + MCP23017LockingArduinoHal(SPIClass &spi, SPISettings spiSettings, ExtensionIOMCP23017 &expander); + + void pinMode(uint32_t pin, uint32_t mode) override; + void digitalWrite(uint32_t pin, uint32_t value) override; + uint32_t digitalRead(uint32_t pin) override; + void attachInterrupt(uint32_t interruptNum, void (*cb)(void), uint32_t mode) override; + void detachInterrupt(uint32_t interruptNum) override; + uint32_t pinToInterrupt(uint32_t pin) override; +#if MCP23017_INT_ESP32_PIN >= 0 + void spiBeginTransaction() override; + void spiEndTransaction() override; +#endif + + private: + ExtensionIOMCP23017 &mcp; + static bool isMcpPin(uint32_t pin); +}; + +#endif // ARCH_ESP32 && USE_MCP23017 diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index e4ec807f823..f98d7b75bc5 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -208,6 +208,10 @@ #define HW_VENDOR meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER_V2 #elif defined(M5STACK_CARDPUTER_ADV) #define HW_VENDOR meshtastic_HardwareModel_M5STACK_CARDPUTER_ADV +#elif defined(MESHNOLOGY_W10) +// master's protobufs predate the MESHNOLOGY_W10 HardwareModel enum (added on develop); report +// PRIVATE_HW until that enum lands here via a protobuf bump. +#define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW #else #define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW #endif diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp index dbc573c95e8..d27960cc914 100644 --- a/src/platform/esp32/main-esp32.cpp +++ b/src/platform/esp32/main-esp32.cpp @@ -15,6 +15,7 @@ #endif #include "esp_mac.h" +#include "freertosinc.h" #include "meshUtils.h" #include "sleep.h" #include "soc/rtc.h" @@ -261,6 +262,8 @@ void cpuDeepSleep(uint32_t msecToWake) // We want RTC peripherals to stay on esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - esp_sleep_enable_timer_wakeup(msecToWake * 1000ULL); // call expects usecs - esp_deep_sleep_start(); // TBD mA sleep current (battery) + // User shutdown (DELAY_FOREVER / portMAX_DELAY): no RTC timer — align with nRF52 system_off semantics. + if (msecToWake != portMAX_DELAY) + esp_sleep_enable_timer_wakeup(msecToWake * 1000ULL); // call expects usecs + esp_deep_sleep_start(); } diff --git a/src/platform/extra_variants/meshnology_w10/variant.cpp b/src/platform/extra_variants/meshnology_w10/variant.cpp new file mode 100644 index 00000000000..3a6c9c8b195 --- /dev/null +++ b/src/platform/extra_variants/meshnology_w10/variant.cpp @@ -0,0 +1,61 @@ +#include "configuration.h" + +#ifdef MESHNOLOGY_W10 + +#include + +#ifdef HAS_I2S +// NOTE: do not include main.h / AudioThread.h here. AudioBoard.h does `using namespace audio_driver`, +// which pulls audio_driver::GpioPin into global scope and collides with Meshtastic's class GpioPin if +// GpioLogic.h is also visible in this TU. Keeping this file codec-only (like the other ES8311 boards) +// avoids that. +#include "AudioBoard.h" +#include "platform/esp32/ExtensionIOMCP23017.h" // mcpIoExpander (NS4150 amp enable on EXIO_PA_CTRL) + +DriverPins PinsAudioBoardES8311; +AudioBoard audioCodecBoard(AudioDriverES8311, PinsAudioBoardES8311); +#endif + +// Meshnology W10 late init: bring up the ES8311 codec so the NS4150 -> speaker path can play +// notification tones over I2S. Called after power->setup() and the radio init, so the I2C bus and +// the MCP23017 are already up. The amp itself is toggled by AudioThread around playback. +void lateInitVariant() +{ +#ifdef HAS_I2S + // Keep the NS4150 amp muted until AudioThread turns it on for playback (avoids idle hiss). + mcpIoExpander.pinMode(EXIO_PA_CTRL, OUTPUT); + mcpIoExpander.digitalWrite(EXIO_PA_CTRL, LOW); + + // I2C: function, Wire (shared bus, ES8311 at 0x18); I2S: function, mclk, bck, ws, dout, din + PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire); + PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN); + + CodecConfig cfg; + cfg.input_device = ADC_INPUT_LINE1; + cfg.output_device = DAC_OUTPUT_ALL; + cfg.i2s.bits = BIT_LENGTH_16BITS; + cfg.i2s.rate = RATE_44K; + audioCodecBoard.begin(cfg); + + // ES8311 register setup (matches the vendor demo / other Meshtastic ES8311 boards) + auto es8311_write_reg = [](uint8_t reg, uint8_t val) { + Wire.beginTransmission(0x18); // ES8311 I2C address + Wire.write(reg); + Wire.write(val); + uint8_t err = Wire.endTransmission(); + if (err != 0) + LOG_WARN("ES8311 reg 0x%02x write failed (err=%d)", reg, err); + }; + es8311_write_reg(0x00, 0x80); // reset, power on + es8311_write_reg(0x01, 0xB5); // MCLK = BCLK + es8311_write_reg(0x02, 0x18); // clock manager, MULT_PRE=3 + es8311_write_reg(0x0D, 0x01); // analog power up + es8311_write_reg(0x12, 0x00); // DAC power up + es8311_write_reg(0x13, 0x10); // enable HP drive + es8311_write_reg(0x32, 0xBF); // DAC volume (0 dB) + es8311_write_reg(0x37, 0x08); // EQ bypass + LOG_INFO("Meshnology W10: ES8311 audio codec initialized"); +#endif // HAS_I2S +} + +#endif // MESHNOLOGY_W10 diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 52e45ccccc9..240dbb0737a 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -20,7 +20,7 @@ static BLEBas blebas; // BAS (Battery Service) helper class instance #ifndef BLE_DFU_SECURE static BLEDfu bledfu; // DFU software update helper service #else -static BLEDfuSecure bledfusecure; // DFU software update helper service +static BLEDfuSecure bledfusecure; // DFU software update helper service #endif // This scratch buffer is used for various bluetooth reads/writes - but it is safe because only one bt operation can be in @@ -258,6 +258,35 @@ int NRF52Bluetooth::getRssi() #define VALID_BLE_TX_POWER(x) \ ((x) == -20 || (x) == -16 || (x) == -12 || (x) == -8 || (x) == -4 || (x) == 0 || (x) == 4 || (x) == 8) +void NRF52Bluetooth::restoreTxPower() +{ +#if defined(NRF52_BLE_TX_POWER) && VALID_BLE_TX_POWER(NRF52_BLE_TX_POWER) + Bluefruit.setTxPower(NRF52_BLE_TX_POWER); +#else + // Bluefruit.begin() default. + Bluefruit.setTxPower(0); +#endif +} + +void NRF52Bluetooth::restoreSecurityState() +{ + if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { + Bluefruit.Security.setPairPasskeyCallback(nullptr); + Bluefruit.Security.setPairCompleteCallback(nullptr); + Bluefruit.Security.setSecuredCallback(nullptr); + Bluefruit.Security.setIOCaps(false, false, false); + // setPairPasskeyCallback() enables MITM even when clearing the callback. + Bluefruit.Security.setMITM(false); + return; + } + + Bluefruit.Security.setIOCaps(true, false, false); + Bluefruit.Security.setMITM(true); + Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onPairingPasskey); + Bluefruit.Security.setPairCompleteCallback(NRF52Bluetooth::onPairingCompleted); + Bluefruit.Security.setSecuredCallback(NRF52Bluetooth::onConnectionSecured); +} + void NRF52Bluetooth::setup() { // Initialise the Bluefruit module @@ -269,9 +298,7 @@ void NRF52Bluetooth::setup() Bluefruit.Advertising.stop(); Bluefruit.Advertising.clearData(); Bluefruit.ScanResponse.clearData(); -#if defined(NRF52_BLE_TX_POWER) && VALID_BLE_TX_POWER(NRF52_BLE_TX_POWER) - Bluefruit.setTxPower(NRF52_BLE_TX_POWER); -#endif + restoreTxPower(); if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN) { configuredPasskey = config.bluetooth.fixed_pin; @@ -281,15 +308,12 @@ void NRF52Bluetooth::setup() configuredPasskey = hwrand % 900000u + 100000u; } auto pinString = std::to_string(configuredPasskey); - LOG_INFO("Bluetooth pin set to '%i'", configuredPasskey); + LOG_DEBUG("Bluetooth pin configured"); Bluefruit.Security.setPIN(pinString.c_str()); - Bluefruit.Security.setIOCaps(true, false, false); - Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onPairingPasskey); - Bluefruit.Security.setPairCompleteCallback(NRF52Bluetooth::onPairingCompleted); - Bluefruit.Security.setSecuredCallback(NRF52Bluetooth::onConnectionSecured); + restoreSecurityState(); meshBleService.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); } else { - Bluefruit.Security.setIOCaps(false, false, false); + restoreSecurityState(); meshBleService.setPermission(SECMODE_OPEN, SECMODE_OPEN); } // Set the advertised device name (keep it short!) @@ -347,6 +371,10 @@ void NRF52Bluetooth::setup() } void NRF52Bluetooth::resumeAdvertising() { + LOG_DEBUG("Resume NRF52 BLE advertising"); + // shutdown() swaps security callbacks, so restore BLE state before advertising. + restoreSecurityState(); + restoreTxPower(); Bluefruit.Advertising.restartOnDisconnect(true); Bluefruit.Advertising.setInterval(32, 668); // in unit of 0.625 ms Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode @@ -371,9 +399,7 @@ void NRF52Bluetooth::onConnectionSecured(uint16_t conn_handle) } bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) { - char passkey1[4] = {passkey[0], passkey[1], passkey[2], '\0'}; - char passkey2[4] = {passkey[3], passkey[4], passkey[5], '\0'}; - LOG_INFO("BLE pair process started with passkey %s %s", passkey1, passkey2); + LOG_INFO("BLE pair process started: match_request=%i", match_request); powerFSM.trigger(EVENT_BLUETOOTH_PAIR); // Get passkey as string @@ -418,14 +444,7 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke #endif passkeyShowing = true; - if (match_request) { - uint32_t start_time = millis(); - while (millis() < start_time + 30000) { - if (!Bluefruit.connected(conn_handle)) - break; - } - } - LOG_INFO("BLE passkey pair: match_request=%i", match_request); + // Pairing completion or disconnect dismisses the passkey UI; blocking here stalls BLE event processing. return true; } @@ -434,6 +453,7 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke // On NRF52Bluetooth::shutdown, we change the pairing callback to this method, to aggressively refuse any connection attempts. bool NRF52Bluetooth::onUnwantedPairing(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) { + LOG_WARN("Rejecting BLE pairing (onUnwantedPairing) - should only fire while Bluetooth is disabled"); NRF52Bluetooth::disconnect(); return false; } diff --git a/src/platform/nrf52/NRF52Bluetooth.h b/src/platform/nrf52/NRF52Bluetooth.h index 630ab05bc80..047bb91f25e 100644 --- a/src/platform/nrf52/NRF52Bluetooth.h +++ b/src/platform/nrf52/NRF52Bluetooth.h @@ -19,7 +19,9 @@ class NRF52Bluetooth : BluetoothApi static void onConnectionSecured(uint16_t conn_handle); static bool onPairingPasskey(uint16_t conn_handle, uint8_t const passkey[6], bool match_request); static void onPairingCompleted(uint16_t conn_handle, uint8_t auth_status); + static void restoreSecurityState(); + static void restoreTxPower(); static bool onUnwantedPairing(uint16_t conn_handle, uint8_t const passkey[6], bool match_request); static void disconnect(); -}; \ No newline at end of file +}; diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index eafd799fc6a..f132640f851 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -85,8 +85,12 @@ #define HW_VENDOR meshtastic_HardwareModel_T_ECHO #elif defined(T_ECHO_LITE) #define HW_VENDOR meshtastic_HardwareModel_T_ECHO_LITE +#elif defined(T_ECHO_CARD) +#define HW_VENDOR meshtastic_HardwareModel_T_ECHO_CARD #elif defined(TTGO_T_ECHO_PLUS) #define HW_VENDOR meshtastic_HardwareModel_T_ECHO_PLUS +#elif defined(T_IMPULSE_PLUS) +#define HW_VENDOR meshtastic_HardwareModel_T_IMPULSE_PLUS #elif defined(ELECROW_ThinkNode_M1) #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M1 #elif defined(ELECROW_ThinkNode_M3) @@ -109,6 +113,8 @@ #define HW_VENDOR meshtastic_HardwareModel_WIO_WM1110 #elif defined(TRACKER_T1000_E) #define HW_VENDOR meshtastic_HardwareModel_TRACKER_T1000_E +#elif defined(MESH_TRACKER_X1) +#define HW_VENDOR meshtastic_HardwareModel_MESH_TRACKER_X1 #elif defined(ME25LS01_4Y10TD) #define HW_VENDOR meshtastic_HardwareModel_ME25LS01_4Y10TD #elif defined(MS24SF1) @@ -117,6 +123,8 @@ #define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW #elif defined(HELTEC_T114) #define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_NODE_T114 +#elif defined(HELTEC_MESH_NODE_T1) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_NODE_T1 #elif defined(MESHLINK) #define HW_VENDOR meshtastic_HardwareModel_MESHLINK #elif defined(SEEED_XIAO_NRF52840_KIT) @@ -127,6 +135,8 @@ #define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_POCKET #elif defined(SEEED_WIO_TRACKER_L1_EINK) #define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1_EINK +#elif defined(SEEED_WIO_TRACKER_L1_PRO_1W) +#define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1_PRO_1W #elif defined(SEEED_WIO_TRACKER_L1) #define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1 #elif defined(HELTEC_MESH_SOLAR) diff --git a/src/platform/portduino/SimRadio.cpp b/src/platform/portduino/SimRadio.cpp index 6e7fe24cbaf..f9fabb6170d 100644 --- a/src/platform/portduino/SimRadio.cpp +++ b/src/platform/portduino/SimRadio.cpp @@ -362,4 +362,10 @@ uint32_t SimRadio::getPacketTime(uint32_t pl, bool received) uint32_t msecs = tPacket * 1000; return msecs; +} + +int16_t SimRadio::getCurrentRSSI() +{ + // Simulated radio - return a reasonable default noise floor + return -120; } \ No newline at end of file diff --git a/src/platform/portduino/SimRadio.h b/src/platform/portduino/SimRadio.h index 6f80989da6f..43b99e92e84 100644 --- a/src/platform/portduino/SimRadio.h +++ b/src/platform/portduino/SimRadio.h @@ -48,6 +48,8 @@ class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThr // Convert Compressed_msg to normal msg and receive it void unpackAndReceive(meshtastic_MeshPacket &p); + int16_t getCurrentRSSI() override; + /** * Debugging counts */ @@ -93,4 +95,4 @@ class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThr virtual uint32_t getPacketTime(uint32_t pl, bool received = false) override; }; -extern SimRadio *simRadio; \ No newline at end of file +extern SimRadio *simRadio; diff --git a/src/power/SGM41562.cpp b/src/power/SGM41562.cpp new file mode 100644 index 00000000000..4f6acf40645 --- /dev/null +++ b/src/power/SGM41562.cpp @@ -0,0 +1,109 @@ +#include "SGM41562.h" + +#ifdef HAS_SGM41562 + +#include + +SGM41562 *sgm41562 = nullptr; + +bool initSGM41562(TwoWire &wire) +{ + if (sgm41562) + return true; + sgm41562 = new SGM41562(); + if (!sgm41562->begin(wire)) { + delete sgm41562; + sgm41562 = nullptr; + return false; + } + return true; +} + +bool SGM41562::readReg(uint8_t reg, uint8_t &value) +{ + wire_->beginTransmission(address_); + wire_->write(reg); + if (wire_->endTransmission(false) != 0) + return false; + if (wire_->requestFrom((int)address_, 1) != 1) + return false; + value = wire_->read(); + return true; +} + +bool SGM41562::writeReg(uint8_t reg, uint8_t value) +{ + wire_->beginTransmission(address_); + wire_->write(reg); + wire_->write(value); + return wire_->endTransmission() == 0; +} + +bool SGM41562::updateReg(uint8_t reg, uint8_t mask, uint8_t value) +{ + uint8_t cur; + if (!readReg(reg, cur)) + return false; + cur = (cur & ~mask) | (value & mask); + return writeReg(reg, cur); +} + +bool SGM41562::begin(TwoWire &wire, uint8_t address) +{ + wire_ = &wire; + address_ = address; + + uint8_t id; + if (!readReg(REG_DEVICE_ID, id)) { + LOG_WARN("SGM41562: I2C read failed at 0x%02X", address_); + return false; + } + if (id != DEVICE_ID_EXPECTED) { + LOG_WARN("SGM41562: unexpected device ID 0x%02X (expected 0x%02X)", id, DEVICE_ID_EXPECTED); + return false; + } + LOG_INFO("SGM41562: detected at 0x%02X (id 0x%02X)", address_, id); + + // Mirror the vendor reference init sequence: PCB OTP off, NTC off, + // watchdog off, charger enabled. These match LilyGo's stock firmware + // for the T-Impulse Plus. + delay(120); + writeReg(REG_SYS_VOLTAGE_REG, 0xB7); + writeReg(REG_MISC_OP_CONTROL, 0x40); + writeReg(REG_CHARGE_TERM_TIMER, 0x1A); + writeReg(REG_POWER_ON_CFG, 0xA4); + + return refresh(); +} + +bool SGM41562::refresh() +{ + uint32_t now = millis(); + if (lastRefreshMs_ != 0 && (now - lastRefreshMs_) < 250) + return true; // cached + lastRefreshMs_ = now == 0 ? 1 : now; + + uint8_t status, fault; + if (!readReg(REG_SYSTEM_STATUS, status)) + return false; + if (!readReg(REG_FAULT, fault)) + return false; + + chargeStatus_ = static_cast((status >> SYS_STATUS_CHRG_SHIFT) & SYS_STATUS_CHRG_MASK); + inputPowerGood_ = (status & SYS_STATUS_PG) != 0; + thermalReg_ = (status & SYS_STATUS_THERM_REG) != 0; + faultMask_ = fault & 0x3F; // bits [7:6] are enter_ship_time config, not faults + return true; +} + +bool SGM41562::setChargeEnable(bool enable) +{ + return updateReg(REG_POWER_ON_CFG, POWER_ON_CFG_CHG_DISABLE, enable ? 0x00 : POWER_ON_CFG_CHG_DISABLE); +} + +bool SGM41562::setShippingModeEnable(bool enable) +{ + return updateReg(REG_MISC_OP_CONTROL, MISC_OP_SHIPPING_MODE, enable ? MISC_OP_SHIPPING_MODE : 0x00); +} + +#endif // HAS_SGM41562 diff --git a/src/power/SGM41562.h b/src/power/SGM41562.h new file mode 100644 index 00000000000..30836ff0926 --- /dev/null +++ b/src/power/SGM41562.h @@ -0,0 +1,102 @@ +#pragma once + +#include "configuration.h" + +#ifdef HAS_SGM41562 + +#include +#include + +// SG Micro SGM41562 — single-cell Li-ion buck charger, I²C-controlled, no +// fuel gauge. This driver exposes status (charging / input good / fault), +// charge enable, and shipping-mode control. Battery voltage/percent still +// come from the platform ADC path; the charger is plumbed in as a +// side-channel for isCharging()/isVbusIn() in AnalogBatteryLevel. +// +// Reference: SGM41562 datasheet (Cmd map + bit fields cross-verified against +// LilyGo's `Cpp_Bus_Driver::Sgm41562xx` driver, which is what their vendor +// example for this board uses). + +#ifndef SGM41562_ADDR +#define SGM41562_ADDR 0x03 // Per datasheet — unusual but correct +#endif + +#ifndef SGM41562_WIRE +#define SGM41562_WIRE Wire1 // Most boards put the PMU on the secondary bus +#endif + +class SGM41562 +{ + public: + enum class ChargeStatus : uint8_t { + NotCharging = 0b00, + Precharge = 0b01, + FastCharge = 0b10, + ChargeDone = 0b11, + }; + + bool begin(TwoWire &wire, uint8_t address = SGM41562_ADDR); + + // Re-read the system status + fault registers. Throttled internally to + // at most one I²C transaction per 250 ms — call as often as you like. + bool refresh(); + + // Status — cached from the most recent refresh(). + ChargeStatus chargeStatus() const { return chargeStatus_; } + bool isCharging() const { return chargeStatus_ == ChargeStatus::Precharge || chargeStatus_ == ChargeStatus::FastCharge; } + bool isChargeDone() const { return chargeStatus_ == ChargeStatus::ChargeDone; } + bool isInputPowerGood() const { return inputPowerGood_; } + bool isThermalRegulation() const { return thermalReg_; } + uint8_t faultMask() const { return faultMask_; } + + // Control. + bool setChargeEnable(bool enable); + bool setShippingModeEnable(bool enable); + + private: + TwoWire *wire_ = nullptr; + uint8_t address_ = SGM41562_ADDR; + uint32_t lastRefreshMs_ = 0; + + ChargeStatus chargeStatus_ = ChargeStatus::NotCharging; + bool inputPowerGood_ = false; + bool thermalReg_ = false; + uint8_t faultMask_ = 0; + + bool readReg(uint8_t reg, uint8_t &value); + bool writeReg(uint8_t reg, uint8_t value); + bool updateReg(uint8_t reg, uint8_t mask, uint8_t value); + + // SGM41562 register addresses + static constexpr uint8_t REG_INPUT_SOURCE = 0x00; + static constexpr uint8_t REG_POWER_ON_CFG = 0x01; + static constexpr uint8_t REG_CHARGE_CURRENT = 0x02; + static constexpr uint8_t REG_DISCHARGE_TERM_CURRENT = 0x03; + static constexpr uint8_t REG_CHARGE_VOLTAGE = 0x04; + static constexpr uint8_t REG_CHARGE_TERM_TIMER = 0x05; + static constexpr uint8_t REG_MISC_OP_CONTROL = 0x06; + static constexpr uint8_t REG_SYS_VOLTAGE_REG = 0x07; + static constexpr uint8_t REG_SYSTEM_STATUS = 0x08; + static constexpr uint8_t REG_FAULT = 0x09; + static constexpr uint8_t REG_I2C_ADDR_MISC = 0x0A; + static constexpr uint8_t REG_DEVICE_ID = 0x0B; + + // Bit positions in REG_POWER_ON_CFG. + static constexpr uint8_t POWER_ON_CFG_CHG_DISABLE = 0x08; // bit 3: 1 = charging disabled + // Bit positions in REG_MISC_OP_CONTROL. + static constexpr uint8_t MISC_OP_SHIPPING_MODE = 0x20; // bit 5: 1 = enter shipping mode + // Bit positions in REG_SYSTEM_STATUS. + static constexpr uint8_t SYS_STATUS_CHRG_SHIFT = 3; + static constexpr uint8_t SYS_STATUS_CHRG_MASK = 0x03; + static constexpr uint8_t SYS_STATUS_PG = 0x02; // bit 1: input power good + static constexpr uint8_t SYS_STATUS_THERM_REG = 0x01; // bit 0: thermal regulation + + static constexpr uint8_t DEVICE_ID_EXPECTED = 0x04; +}; + +extern SGM41562 *sgm41562; + +// Lazy-instantiate the global on the supplied wire. Returns true on success. +bool initSGM41562(TwoWire &wire); + +#endif // HAS_SGM41562 diff --git a/src/sleep.cpp b/src/sleep.cpp index 64bd0c48033..39cfb6ac288 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -252,20 +252,26 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN digitalWrite(SDCARD_CS, LOW); #endif -#ifdef TRACKER_T1000_E +#if defined(TRACKER_T1000_E) || defined(MESH_TRACKER_X1) #ifdef GNSS_AIROHA digitalWrite(GPS_VRTC_EN, LOW); digitalWrite(PIN_GPS_RESET, LOW); digitalWrite(GPS_SLEEP_INT, LOW); digitalWrite(GPS_RTC_INT, LOW); +#ifdef GPS_RESETB_OUT pinMode(GPS_RESETB_OUT, OUTPUT); digitalWrite(GPS_RESETB_OUT, LOW); #endif +#endif #ifdef BUZZER_EN_PIN digitalWrite(BUZZER_EN_PIN, LOW); #endif +#ifdef PIN_DRV_EN + digitalWrite(PIN_DRV_EN, LOW); +#endif + #ifdef PIN_3V3_EN digitalWrite(PIN_3V3_EN, LOW); #endif @@ -379,9 +385,9 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r { // LOG_DEBUG("Enter light sleep"); - // LORA_DIO1 is an extended IO pin. Setting it as a wake-up pin will cause problems, such as the indicator device not entering - // LightSleep. -#if defined(SENSECAP_INDICATOR) + // LORA_DIO1 is an extended IO pin (on an I/O expander). Setting it as a wake-up pin will cause problems, + // such as the device not entering light sleep. Boards opt in with LORA_DIO1_EXTENDED_IO in their variant. +#if defined(LORA_DIO1_EXTENDED_IO) return ESP_SLEEP_WAKEUP_TIMER; #endif @@ -550,8 +556,10 @@ bool shouldLoraWake(uint32_t msecToWake) void enableLoraInterrupt() { +#if defined(LORA_DIO1_EXTENDED_IO) + // DIO1 is a virtual pin on an I/O expander - it cannot be a GPIO wakeup source +#elif SOC_PM_SUPPORT_EXT_WAKEUP && defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) esp_err_t res; -#if SOC_PM_SUPPORT_EXT_WAKEUP && defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) res = gpio_pulldown_en((gpio_num_t)LORA_DIO1); if (res != ESP_OK) { LOG_ERROR("gpio_pulldown_en(LORA_DIO1) result %d", res); diff --git a/test/test_position_precision/test_main.cpp b/test/test_position_precision/test_main.cpp index 4f5aecfda97..bb611817795 100644 --- a/test/test_position_precision/test_main.cpp +++ b/test/test_position_precision/test_main.cpp @@ -19,6 +19,16 @@ static meshtastic_Position makePosition() return position; } +static meshtastic_Channel makeChannel(meshtastic_Channel_Role role, bool hasModuleSettings, uint32_t positionPrecision) +{ + meshtastic_Channel channel = meshtastic_Channel_init_default; + channel.has_settings = true; + channel.role = role; + channel.settings.has_module_settings = hasModuleSettings; + channel.settings.module_settings.position_precision = positionPrecision; + return channel; +} + static void test_applyPositionPrecision_clampsLatLonAndSetsPrecisionBits() { meshtastic_Position position = makePosition(); @@ -80,6 +90,35 @@ static void test_applyPositionPrecision_reencodesPositionPacket() TEST_ASSERT_EQUAL_UINT32(16, decoded.precision_bits); } +static void test_getPositionPrecisionForChannel_explicitPrecisionIsHonored() +{ + meshtastic_Channel channel = makeChannel(meshtastic_Channel_Role_PRIMARY, true, 16); + + TEST_ASSERT_EQUAL_UINT32(16, getPositionPrecisionForChannel(channel)); +} + +static void test_getPositionPrecisionForChannel_explicitZeroDisablesPrimary() +{ + meshtastic_Channel channel = makeChannel(meshtastic_Channel_Role_PRIMARY, true, 0); + + TEST_ASSERT_EQUAL_UINT32(0, getPositionPrecisionForChannel(channel)); +} + +static void test_getPositionPrecisionForChannel_primaryWithoutModuleSettingsFailsClosed() +{ + // Regression guard for #10509: precision 32 below must be ignored (no module settings). + meshtastic_Channel channel = makeChannel(meshtastic_Channel_Role_PRIMARY, false, 32); + + TEST_ASSERT_EQUAL_UINT32(0, getPositionPrecisionForChannel(channel)); +} + +static void test_getPositionPrecisionForChannel_secondaryWithoutModuleSettingsFailsClosed() +{ + meshtastic_Channel channel = makeChannel(meshtastic_Channel_Role_SECONDARY, false, 32); + + TEST_ASSERT_EQUAL_UINT32(0, getPositionPrecisionForChannel(channel)); +} + void setUp(void) {} void tearDown(void) {} @@ -93,6 +132,10 @@ void setup() RUN_TEST(test_applyPositionPrecision_fullPrecisionKeepsLatLon); RUN_TEST(test_applyPositionPrecision_zeroScrubsLocationButKeepsTime); RUN_TEST(test_applyPositionPrecision_reencodesPositionPacket); + RUN_TEST(test_getPositionPrecisionForChannel_explicitPrecisionIsHonored); + RUN_TEST(test_getPositionPrecisionForChannel_explicitZeroDisablesPrimary); + RUN_TEST(test_getPositionPrecisionForChannel_primaryWithoutModuleSettingsFailsClosed); + RUN_TEST(test_getPositionPrecisionForChannel_secondaryWithoutModuleSettingsFailsClosed); exit(UNITY_END()); } diff --git a/test/test_radio/test_main.cpp b/test/test_radio/test_main.cpp index fbe2b1b1304..4acd783a477 100644 --- a/test/test_radio/test_main.cpp +++ b/test/test_radio/test_main.cpp @@ -1,10 +1,54 @@ #include "MeshRadio.h" +#include "MeshService.h" +#include "NodeDB.h" #include "RadioInterface.h" +#include "RadioLibInterface.h" #include "TestUtil.h" +#include #include #include "meshtastic/config.pb.h" +class MockMeshService : public MeshService +{ + public: + void sendClientNotification(meshtastic_ClientNotification *n) override { releaseClientNotificationToPool(n); } +}; + +static MockMeshService *mockMeshService; + +static LockingArduinoHal *getTestHal() +{ + static LockingArduinoHal hal(SPI, SPISettings(1000000, MSBFIRST, SPI_MODE0)); + return &hal; +} + +class TestableRadioLibInterface : public RadioLibInterface +{ + public: + TestableRadioLibInterface() : RadioLibInterface(getTestHal(), RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, nullptr) {} + + void seedNoiseFloorForTest() + { + noiseFloorSamples[0] = -110; + currentSampleIndex = 1; + isNoiseFloorBufferFull = false; + lastNoiseFloorUpdate = 1234; + currentNoiseFloor = -110; + } + + uint32_t getLastNoiseFloorUpdateForTest() const { return lastNoiseFloorUpdate; } + + protected: + void disableInterrupt() override {} + void enableInterrupt(void (*)()) override {} + bool isChannelActive() override { return false; } + bool isActivelyReceiving() override { return false; } + void addReceiveMetadata(meshtastic_MeshPacket *) override {} + uint32_t getPacketTime(uint32_t, bool) override { return 0; } + int16_t getCurrentRSSI() override { return NOISE_FLOOR_DEFAULT; } +}; + static void test_bwCodeToKHz_specialMappings() { TEST_ASSERT_FLOAT_WITHIN(0.0001f, 31.25f, bwCodeToKHz(31)); @@ -77,8 +121,59 @@ static void test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegion TEST_ASSERT_EQUAL_UINT32(11, cfg.spread_factor); } -void setUp(void) {} -void tearDown(void) {} +static void configureLongFastUs(float frequencyOffset = 0.0f) +{ + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + config.lora.frequency_offset = frequencyOffset; +} + +static void test_radioLibReconfigureResetsNoiseFloorWhenFrequencyChanges() +{ + TestableRadioLibInterface testRadioLib; + configureLongFastUs(); + testRadioLib.reconfigure(); + testRadioLib.seedNoiseFloorForTest(); + + config.lora.frequency_offset = 0.125f; + testRadioLib.reconfigure(); + + TEST_ASSERT_FALSE(testRadioLib.hasNoiseFloorSamples()); + TEST_ASSERT_EQUAL_INT32(-120, testRadioLib.getNoiseFloor()); + TEST_ASSERT_EQUAL_UINT32(0, testRadioLib.getLastNoiseFloorUpdateForTest()); +} + +static void test_radioLibReconfigureKeepsNoiseFloorWhenFrequencyUnchanged() +{ + TestableRadioLibInterface testRadioLib; + configureLongFastUs(); + testRadioLib.reconfigure(); + testRadioLib.seedNoiseFloorForTest(); + + testRadioLib.reconfigure(); + + TEST_ASSERT_TRUE(testRadioLib.hasNoiseFloorSamples()); + TEST_ASSERT_EQUAL_INT32(-110, testRadioLib.getNoiseFloor()); + TEST_ASSERT_EQUAL_UINT32(1234, testRadioLib.getLastNoiseFloorUpdateForTest()); +} + +void setUp(void) +{ + mockMeshService = new MockMeshService(); + service = mockMeshService; + + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; + initRegion(); +} + +void tearDown(void) +{ + service = nullptr; + delete mockMeshService; + mockMeshService = nullptr; +} void setup() { @@ -94,6 +189,8 @@ void setup() RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_nonWideRegion); RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_wideRegion); RUN_TEST(test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegionSpan); + RUN_TEST(test_radioLibReconfigureResetsNoiseFloorWhenFrequencyChanges); + RUN_TEST(test_radioLibReconfigureKeepsNoiseFloorWhenFrequencyUnchanged); exit(UNITY_END()); } diff --git a/test/test_rtc/test_main.cpp b/test/test_rtc/test_main.cpp new file mode 100644 index 00000000000..02cad01c407 --- /dev/null +++ b/test/test_rtc/test_main.cpp @@ -0,0 +1,82 @@ +#include "TestUtil.h" +#include "gps/RTC.h" +#include +#include +#include + +// Regression coverage for issue #9828: on boards without a hardware RTC (e.g. RP2040), +// gettimeofday() can return uptime seconds rather than wall-clock time. A later readFromRTC() +// must not overwrite a higher-quality network/GPS time with that value, but it should still seed +// the clock when nothing better exists yet. +// +// The native test build compiles the RV3028 hardware-RTC branch (variants/native/portduino +// defines RV3028_RTC), so these tests use setReadFromRTCUseSystemTimeForTests() to force the +// no-hardware-RTC fallback path and setRTCSystemTimeForTests() to inject a deterministic clock. + +static const uint32_t kAllowedDriftSeconds = 2; +static const time_t kUptimeSeconds = 21; // what gettimeofday() returns on RP2040 without a real clock + +// A clearly-valid wall-clock epoch, safely inside any BUILD_EPOCH validity window. +static time_t makeValidEpoch() +{ + return time(NULL) + SEC_PER_DAY; +} + +void setUp(void) +{ + resetRTCStateForTests(); +} + +void tearDown(void) +{ + resetRTCStateForTests(); +} + +// A higher-quality network time must survive a later system-time read that only knows uptime. +static void test_readFromRTC_preserves_better_network_time(void) +{ + const time_t networkEpoch = makeValidEpoch(); + struct timeval networkTime; + networkTime.tv_sec = networkEpoch; + networkTime.tv_usec = 0; + TEST_ASSERT_EQUAL_INT(RTCSetResultSuccess, perhapsSetRTC(RTCQualityFromNet, &networkTime)); + + // Simulate a later readFromRTC() falling back to a system clock that only knows uptime. + struct timeval uptime; + uptime.tv_sec = kUptimeSeconds; + uptime.tv_usec = 0; + setRTCSystemTimeForTests(&uptime); + setReadFromRTCUseSystemTimeForTests(true); + + TEST_ASSERT_EQUAL_INT(RTCSetResultSuccess, readFromRTC()); + TEST_ASSERT_EQUAL_INT(RTCQualityFromNet, getRTCQuality()); + TEST_ASSERT_UINT32_WITHIN(kAllowedDriftSeconds, (uint32_t)networkEpoch, getValidTime(RTCQualityFromNet)); +} + +// Before any higher-quality source exists, the fallback should still seed the clock. +static void test_readFromRTC_initializes_time_when_no_better_source(void) +{ + const time_t systemEpoch = makeValidEpoch(); + struct timeval systemTime; + systemTime.tv_sec = systemEpoch; + systemTime.tv_usec = 0; + setRTCSystemTimeForTests(&systemTime); + setReadFromRTCUseSystemTimeForTests(true); + + TEST_ASSERT_EQUAL_INT(RTCSetResultSuccess, readFromRTC()); + TEST_ASSERT_EQUAL_INT(RTCQualityNone, getRTCQuality()); + TEST_ASSERT_UINT32_WITHIN(kAllowedDriftSeconds, (uint32_t)systemEpoch, getTime()); +} + +void setup() +{ + delay(10); + initializeTestEnvironment(); + + UNITY_BEGIN(); + RUN_TEST(test_readFromRTC_preserves_better_network_time); + RUN_TEST(test_readFromRTC_initializes_time_when_no_better_source); + exit(UNITY_END()); +} + +void loop() {} diff --git a/variants/esp32/chatter2/platformio.ini b/variants/esp32/chatter2/platformio.ini index b0adeee4b7e..d68cc5e2ad9 100644 --- a/variants/esp32/chatter2/platformio.ini +++ b/variants/esp32/chatter2/platformio.ini @@ -12,4 +12,4 @@ build_flags = lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 diff --git a/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini index 91ae0017b94..69c71969429 100644 --- a/variants/esp32/esp32-common.ini +++ b/variants/esp32/esp32-common.ini @@ -72,7 +72,7 @@ lib_deps = # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino h2zero/NimBLE-Arduino@1.4.3 # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master - https://github.com/dbinfrago/libpax/archive/df424747f9acb86ab07c5a206ded1e8e3650726a.zip + https://github.com/dbinfrago/libpax/archive/17302340f100efbc4bee5022ecc72047d6d93ed4.zip # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib lewisxhe/XPowersLib@0.3.3 # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto diff --git a/variants/esp32/m5stack_core/platformio.ini b/variants/esp32/m5stack_core/platformio.ini index 4f0b556acba..aa667d57b81 100644 --- a/variants/esp32/m5stack_core/platformio.ini +++ b/variants/esp32/m5stack_core/platformio.ini @@ -35,4 +35,4 @@ lib_ignore = lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 diff --git a/variants/esp32/wiphone/platformio.ini b/variants/esp32/wiphone/platformio.ini index b1c1f8bf749..2114d965e71 100644 --- a/variants/esp32/wiphone/platformio.ini +++ b/variants/esp32/wiphone/platformio.ini @@ -11,7 +11,7 @@ build_flags = lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 # renovate: datasource=custom.pio depName=SX1509 IO Expander packageName=sparkfun/library/SX1509 IO Expander sparkfun/SX1509 IO Expander@3.0.6 # renovate: datasource=custom.pio depName=APA102 packageName=pololu/library/APA102 diff --git a/variants/esp32s3/ELECROW-ThinkNode-G3/platformio.ini b/variants/esp32s3/ELECROW-ThinkNode-G3/platformio.ini index 4f94f5d3927..cbfdfe9034d 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-G3/platformio.ini +++ b/variants/esp32s3/ELECROW-ThinkNode-G3/platformio.ini @@ -21,4 +21,4 @@ build_src_filter = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=github-tags depName=ESP32-CH390 packageName=meshtastic/ESP32-CH390 - https://github.com/meshtastic/ESP32-CH390/archive/refs/tags/v1.0.1.zip + https://github.com/meshtastic/ESP32-CH390/archive/v1.1.0.zip diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h index 2d02c7f27be..6e12fdf8a55 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h @@ -18,7 +18,9 @@ #define BATTERY_PIN 8 #define ADC_CHANNEL ADC1_GPIO8_CHANNEL -#define ADC_MULTIPLIER 2.0 // 2.0 + 10% for correction of display undervoltage. +#define ADC_MULTIPLIER 2.0 + +#define OCV_ARRAY 4100, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100 #define PIN_BUZZER 9 diff --git a/variants/esp32s3/ELECROW-ThinkNode-M7/platformio.ini b/variants/esp32s3/ELECROW-ThinkNode-M7/platformio.ini index 68fe6818249..565a9dca24b 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M7/platformio.ini +++ b/variants/esp32s3/ELECROW-ThinkNode-M7/platformio.ini @@ -20,4 +20,4 @@ build_src_filter = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=github-tags depName=ESP32-CH390 packageName=meshtastic/ESP32-CH390 - https://github.com/meshtastic/ESP32-CH390/archive/refs/tags/v1.0.1.zip \ No newline at end of file + https://github.com/meshtastic/ESP32-CH390/archive/v1.1.0.zip diff --git a/variants/esp32s3/ELECROW-ThinkNode-M7/variant.cpp b/variants/esp32s3/ELECROW-ThinkNode-M7/variant.cpp index c6ff6b8d8a8..9c190c6dc03 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M7/variant.cpp +++ b/variants/esp32s3/ELECROW-ThinkNode-M7/variant.cpp @@ -4,3 +4,11 @@ void variantDefaultConfig() { config.network.eth_enabled = true; } + +void initVariant() +{ + pinMode(LED_PAIRING, OUTPUT); + digitalWrite(LED_PAIRING, !LED_STATE_ON); // Turn off the LED to start + pinMode(LED_LORA, OUTPUT); + digitalWrite(LED_LORA, !LED_STATE_ON); // Turn off the LED to start +} \ No newline at end of file diff --git a/variants/esp32s3/ELECROW-ThinkNode-M7/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M7/variant.h index 9724b20fab3..b0bd7aeea8c 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M7/variant.h +++ b/variants/esp32s3/ELECROW-ThinkNode-M7/variant.h @@ -6,11 +6,12 @@ #define UART_TX 43 #define UART_RX 44 -#define WIFI_LED 3 -#define WIFI_STATE_ON 0 +#define LED_PAIRING 46 +#define LED_LORA 46 -#define LED_PIN 46 +#define LED_PIN 3 #define LED_STATE_ON 0 +#define LED_STATE_OFF 1 #define BUTTON_PIN 4 #define BUTTON_ACTIVE_LOW true #define BUTTON_ACTIVE_PULLUP true diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini index 790f0292440..9490faa6cf1 100644 --- a/variants/esp32s3/heltec_v4/platformio.ini +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -133,6 +133,6 @@ build_flags = lib_deps = ${heltec_v4_base.lib_deps} ${device-ui_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 # renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip \ No newline at end of file diff --git a/variants/esp32s3/heltec_v4_r8/platformio.ini b/variants/esp32s3/heltec_v4_r8/platformio.ini index 7799acf437d..4198df454d9 100644 --- a/variants/esp32s3/heltec_v4_r8/platformio.ini +++ b/variants/esp32s3/heltec_v4_r8/platformio.ini @@ -140,6 +140,6 @@ build_flags = lib_deps = ${heltec_v4_r8_base.lib_deps} ${device-ui_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 # renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master https://github.com/Quency-D/chsc6x/archive/3b2b6cebf3177b3e2c33d06e07909b0b10159516.zip \ No newline at end of file diff --git a/variants/esp32s3/heltec_wireless_tracker/platformio.ini b/variants/esp32s3/heltec_wireless_tracker/platformio.ini index 1450bb45ce3..1f99f377e0c 100644 --- a/variants/esp32s3/heltec_wireless_tracker/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker/platformio.ini @@ -24,4 +24,4 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 diff --git a/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini b/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini index ffcc1fe953f..4613a8e9272 100644 --- a/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini @@ -22,4 +22,4 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini index 53f698c9ea2..d8afbfe94b9 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini @@ -21,4 +21,4 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 diff --git a/variants/esp32s3/m5stack_cardputer_adv/platformio.ini b/variants/esp32s3/m5stack_cardputer_adv/platformio.ini index 69c4f52a5bd..a38317e5be3 100644 --- a/variants/esp32s3/m5stack_cardputer_adv/platformio.ini +++ b/variants/esp32s3/m5stack_cardputer_adv/platformio.ini @@ -15,7 +15,7 @@ lib_deps = # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver - https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip + https://github.com/pschatzmann/arduino-audio-driver/archive/v0.3.0.zip # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM diff --git a/variants/esp32s3/m5stack_cardputer_adv/variant.h b/variants/esp32s3/m5stack_cardputer_adv/variant.h index 48437cd1346..778d95f200f 100644 --- a/variants/esp32s3/m5stack_cardputer_adv/variant.h +++ b/variants/esp32s3/m5stack_cardputer_adv/variant.h @@ -59,6 +59,14 @@ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define TCXO_OPTIONAL +// SD card slot — shares the SPI bus with the LoRa radio (separate chip select). +// The default SPI instance is used; spiLock arbitrates access between radio and SD. +#define HAS_SDCARD +#define SPI_SCK 40 +#define SPI_MISO 39 +#define SPI_MOSI 14 +#define SDCARD_CS 12 + #undef GPS_RX_PIN #undef GPS_TX_PIN #define GPS_RX_PIN 15 diff --git a/variants/esp32s3/mesh-tab/platformio.ini b/variants/esp32s3/mesh-tab/platformio.ini index fcb58d36ac3..fe508656233 100644 --- a/variants/esp32s3/mesh-tab/platformio.ini +++ b/variants/esp32s3/mesh-tab/platformio.ini @@ -55,7 +55,7 @@ lib_deps = ${esp32s3_base.lib_deps} ${device-ui_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 [mesh_tab_xpt2046] extends = mesh_tab_base diff --git a/variants/esp32s3/meshnology-w10/pins_arduino.h b/variants/esp32s3/meshnology-w10/pins_arduino.h new file mode 100644 index 00000000000..6bc0b156e58 --- /dev/null +++ b/variants/esp32s3/meshnology-w10/pins_arduino.h @@ -0,0 +1,62 @@ +// Meshnology W10 - shadows the generic esp32s3 variant pins. +// Deliberately omits PIN_RGB_LED / LED_BUILTIN / RGB_BUILTIN: the generic definitions make the +// core's digitalWrite() reference the RMT-backed RGB LED HAL, which fails to link against this +// build's trimmed FreeRTOS config (no xEventGroupSetBitsFromISR). +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "soc/soc_caps.h" +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 8; +static const uint8_t SCL = 7; + +// Shared LoRa/LCD SPI bus (E22_NSS is the default SS) +static const uint8_t SS = 14; +static const uint8_t MOSI = 13; +static const uint8_t MISO = 11; +static const uint8_t SCK = 12; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/meshnology-w10/platformio.ini b/variants/esp32s3/meshnology-w10/platformio.ini new file mode 100644 index 00000000000..27194a06c7f --- /dev/null +++ b/variants/esp32s3/meshnology-w10/platformio.ini @@ -0,0 +1,41 @@ +[env:meshnology_w10] +custom_meshtastic_hw_model = 140 +custom_meshtastic_hw_model_slug = MESHNOLOGY_W10 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Meshnology W10 +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + +; ESP32-S3R8: 8 MB OPI PSRAM, W25Q128JVSIQ = 16 MB QIO flash (pg1) +board = esp32-s3-devkitc-1 +board_level = pr +board_build.partitions = default_16MB.csv +board_upload.flash_size = 16MB +board_build.flash_mode = qio +board_build.psram_type = opi +board_build.arduino.memory_type = qio_opi + +extends = esp32s3_base +build_flags = + ${esp32s3_base.build_flags} + -D MESHNOLOGY_W10 + -D ARDUINO_USB_CDC_ON_BOOT=1 + -I variants/esp32s3/meshnology-w10 + +lib_deps = + ${esp32s3_base.lib_deps} + ; ST7789/ST7796 TFT (USE_TFTDISPLAY) + ; renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.21 + ; PCF85063 RTC driver (PCF85063_RTC) + ; renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 + ; ES8311 audio codec + I2S notification tones (HAS_I2S) + ; renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver + https://github.com/pschatzmann/arduino-audio-driver/archive/v0.3.0.zip + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip + # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM + earlephilhower/ESP8266SAM@1.1.0 diff --git a/variants/esp32s3/meshnology-w10/variant.h b/variants/esp32s3/meshnology-w10/variant.h new file mode 100644 index 00000000000..98677b218c5 --- /dev/null +++ b/variants/esp32s3/meshnology-w10/variant.h @@ -0,0 +1,171 @@ +#pragma once + +// Meshnology W10 "LoRa AIOT Dev Kit" - ESP32-S3R8 + EBYTE E22-900MM22S (SX1262) + AXP2101 PMIC + +// Quectel L76KB-A58 GPS + SPI TFT. The SX1262 RESET/DIO1/BUSY and the LCD reset are wired to an +// MCP23017 I2C expander (0x20) whose /INT is not routed to the MCU, so DIO1 uses a software poll. + +// ─── I2C bus ────────────────────────────────────────────────────────────────── +// Shared by: AXP2101 PMIC, MCP23017 I/O expander, PCF85063ATL RTC, QMI8658 IMU, +// ES8311 codec, SHT41 temp/humidity sensor (pg1: ESP_SDA=GPIO8, ESP_SCL=GPIO7) +#define I2C_SDA 8 +#define I2C_SCL 7 + +// ─── Power management ───────────────────────────────────────────────────────── +// AXP2101 PMIC on I2C (pg3). AXP_IRQ → EXIO5 (expander, whose /INT is not routed to the +// ESP32), so no PMU_IRQ is possible; battery state is polled via the AXP2101 fuel gauge. +// There is no direct battery ADC - the PMIC is the only voltage source. +#define HAS_AXP2101 + +// ─── RTC ────────────────────────────────────────────────────────────────────── +// PCF85063ATL on I2C (pg3); RTC_INT → EXIO4 (unused) +#define PCF85063_RTC 0x51 + +// ─── I/O expander ───────────────────────────────────────────────────────────── +// U7 is drawn as "TCA9555PWR(UMW)" on schematic V1.1, but production boards use the MCP23017 +// register map (V1.2 placement labels the part MCP23017T-E, and all vendor firmware/demo code +// drives IODIR 0x00/01, GPIO 0x12/13, OLAT 0x14/15). A0/A1/A2 = 0 → I2C address 0x20. +// The expander /INT output is NOT routed to the ESP32. +#define USE_MCP23017 +#define MCP23017_ADDR 0x20 +#define MCP23017_VPIN_BASE 100 // RadioLib virtual pins 100..115 = expander GPA0..GPB7 +#define MCP23017_INT_ESP32_PIN (-1) // /INT not wired to any ESP32 GPIO +#if MCP23017_INT_ESP32_PIN < 0 +// No hardware DIO1 interrupt possible: SX126x IRQ status register is polled from the radio thread +#define LORA_DIO1_SOFTWARE_POLL 1 +#endif + +// LORA_DIO1 is an expander pin, not an ESP32 GPIO, so it can't be used as a sleep/GPIO wakeup source +// (shared capability; see its use in sleep.cpp). +#define LORA_DIO1_EXTENDED_IO + +// Expander pin map (pg2 "I/O Extensions"; EXIO0..7 = GPA0..7 / P00..P07, EXIO8..15 = GPB0..7 / P10..P17) +// Not wired up here: EXIO0 CAM_PWDN, EXIO2 TP_INT, EXIO5 AXP_IRQ, EXIO6 SYS_OUT, +// EXIO11 GPS RESET_N (driver FET Q3 unpopulated), EXIO13 TP_RST +#define EXIO_LCD_RST 1 // GPA1: LCD reset +#define EXIO_LORA_NRST 3 // GPA3: E22 NRST +#define EXIO_RTC_INT 4 // GPA4: PCF85063 INT (unused) +#define EXIO_PA_CTRL 7 // GPA7: NS4150 speaker amp enable (driven by AudioThread during playback) +#define EXIO_IMU_INT1 8 // GPB0: QMI8658 INT1 (input only, no MCU interrupt) +#define EXIO_LORA_DIO1 9 // GPB1: E22 DIO1 +#define EXIO_LORA_BUSY 10 // GPB2: E22 BUSY +#define EXIO_GPS_WAKE 12 // GPB4: L76KB WAKE_UP (driven high at boot) + +// ─── LoRa radio ─────────────────────────────────────────────────────────────── +// EBYTE E22-900MM22S (SX1262) - pg4 U10. SPI shared with the LCD, separate chip selects. +// Bus pins via 0R links: E22_SCK=GPIO12, E22_MOSI=GPIO13, E22_MISO=GPIO11, E22_NSS=GPIO14 (pg4) +#define USE_SX1262 +#define LORA_SCK 12 +#define LORA_MOSI 13 +#define LORA_MISO 11 +#define LORA_CS 14 + +// Control lines route through the MCP23017 (pg4: E22_NRST=EXIO3, E22_DIO1=EXIO9, E22_BUSY=EXIO10), +// handled as RadioLib virtual pins by MCP23017LockingArduinoHal +#define LORA_DIO1 (MCP23017_VPIN_BASE + EXIO_LORA_DIO1) // 109 +#define LORA_BUSY (MCP23017_VPIN_BASE + EXIO_LORA_BUSY) // 110 +#define LORA_RESET (MCP23017_VPIN_BASE + EXIO_LORA_NRST) // 103 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_BUSY +#define SX126X_RESET LORA_RESET + +// RF switch is fully hardware-automatic on this board: DIO2 drives TXEN directly and RXEN through +// inverting FET Q4 (pg4). Do not assign TXEN/RXEN GPIOs. +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_RXEN RADIOLIB_NC + +// E22-900MM22S uses a plain 32 MHz crystal (no TCXO) - SX126X_DIO3_TCXO_VOLTAGE deliberately not defined +#define SX126X_MAX_POWER 22 + +// ─── Display ────────────────────────────────────────────────────────────────── +// SPI TFT on the "OLED"-silkscreened header / LCD FPC, sharing the LoRa SPI bus (pg1). +// Vendor ships two panels on identical pins; the default kit has the ST7789 1.54" IPS 240x240. +// Build with -D MESHNOLOGY_W10_LCD_ST7796_35 for the 3.5" ST7796 320x480 panel instead. +#define TFT_CS 10 // pg1: OLED_CS=GPIO10 +#define TFT_DC 16 // pg1: OLED_DC=GPIO16 +#define TFT_BL 6 // pg1: OLED_BL=GPIO6 (backlight PWM) +#define TFT_RST -1 // panel reset is EXIO1 on the expander, toggled in mcp23017EarlyInit() +#define USE_TFTDISPLAY 1 + +#define SPI_FREQUENCY 75000000 +#define SPI_READ_FREQUENCY 16000000 + +#ifdef MESHNOLOGY_W10_LCD_ST7796_35 +// ST7796 3.5" 320x480 (capacitive touch on I2C - not enabled yet) +#define ST7796_SPI_HOST SPI2_HOST +#define ST7796_CS TFT_CS +#define ST7796_RS TFT_DC +#define ST7796_SDA LORA_MOSI +#define ST7796_SCK LORA_SCK +#define ST7796_MISO LORA_MISO +#define ST7796_RESET TFT_RST +#define ST7796_BL TFT_BL +#define ST7796_BUSY -1 +#define TFT_WIDTH 320 +#define TFT_HEIGHT 480 +#define TFT_OFFSET_ROTATION 3 +#else +// ST7789 1.54" IPS 240x240 (default kit panel) +#define ST7789_SPI_HOST SPI2_HOST +#define ST7789_CS TFT_CS +#define ST7789_RS TFT_DC +#define ST7789_SDA LORA_MOSI +#define ST7789_SCK LORA_SCK +#define ST7789_MISO LORA_MISO +#define ST7789_RESET TFT_RST +#define ST7789_BL TFT_BL +#define ST7789_BUSY -1 +#define TFT_WIDTH 240 +#define TFT_HEIGHT 240 +#define TFT_OFFSET_ROTATION 1 +#endif +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 + +// ─── GPS ────────────────────────────────────────────────────────────────────── +// Quectel L76KB-A58 on UART0 pins (pg4: GPS_TXD→U0RXD=GPIO44, GPS_RXD→U0TXD=GPIO43; console is USB) +// 1PPS only drives LED3 (pg3); RESET_N driver FET is unpopulated; WAKE_UP = EXIO12, driven high at boot +#define HAS_GPS 1 +#define GPS_RX_PIN 44 +#define GPS_TX_PIN 43 +#define GPS_BAUDRATE 9600 + +// ─── User input ─────────────────────────────────────────────────────────────── +// pg1: SW2 pulls GPIO0 to GND (BOOT doubles as user button). SW3 is the AXP2101 power button. +#define BUTTON_PIN 0 +#define BUTTON_NEED_PULLUP + +// ─── LED ────────────────────────────────────────────────────────────────────── +// TX1812 (WS2812-compatible) RGB LED on GPIO48 via 33R (pg1, U35). The other LEDs are +// hardware-driven: AXP2101 CHGLED, VSYS power LED, GPS 1PPS LED, UART0 TX/RX activity LEDs. +// TODO: enabling HAS_NEOPIXEL currently fails to link (Adafruit NeoPixel pulls in the Arduino RMT +// HAL, which needs xEventGroupSetBitsFromISR - not present in this build's FreeRTOS config) +// #define HAS_NEOPIXEL +// #define NEOPIXEL_COUNT 1 +// #define NEOPIXEL_DATA 48 +// #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) + +// ─── Audio ──────────────────────────────────────────────────────────────────── +// ES8311 codec (I2C 0x18) -> NS4150 amp -> speaker, initialized in the variant's lateInitVariant(). +// Used for notification tones / ringtones over the I2S "buzzer" path (turn on the +// use_i2s_as_buzzer external-notification option). Codec2 voice is SX1280-only, so it does not +// apply to this sub-GHz board. The NS4150 amp enable is on the MCP23017 (EXIO_PA_CTRL / GPA7) and +// is toggled by AudioThread around playback. pg3 I2S wiring below. +#define HAS_I2S +#define DAC_I2S_MCLK 1 // pg3: ES8311 MCLK +#define DAC_I2S_BCK 2 // pg3: ES8311 BCLK/SCLK +#define DAC_I2S_WS 4 // pg3: ES8311 LRCK +#define DAC_I2S_DOUT 5 // pg3: playback data (ESP32 -> ES8311) +#define DAC_I2S_DIN 3 // pg3: record data (ES8311 -> ESP32) +// AudioThread powers the NS4150 amp on/off around playback via this (opt-in) hook. +#define AUDIO_AMP_ENABLE(on) mcpIoExpander.digitalWrite(EXIO_PA_CTRL, (on) ? HIGH : LOW) + +// ─── On-board peripherals not wired up yet ──────────────────────────────────── +// SHT41 temp/humidity (0x44) and QMI8658 IMU (0x6B): auto-detected on the I2C scan +// microSD: CS on GPIO9, shares the LCD/LoRa SPI bus - not enabled +// Camera interface: GPIO38-42/45-48 (pg1) - not enabled + +// ─── Board identity ─────────────────────────────────────────────────────────── +#define MESHNOLOGY_W10 1 diff --git a/variants/esp32s3/picomputer-s3/platformio.ini b/variants/esp32s3/picomputer-s3/platformio.ini index b5a4ff178f2..f47d7990d7f 100644 --- a/variants/esp32s3/picomputer-s3/platformio.ini +++ b/variants/esp32s3/picomputer-s3/platformio.ini @@ -25,7 +25,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 build_src_filter = ${esp32s3_base.build_src_filter} @@ -37,6 +37,9 @@ extends = env:picomputer-s3 build_flags = ${env:picomputer-s3.build_flags} -D MESHTASTIC_EXCLUDE_WEBSERVER=1 + ; device-ui's I2CKeyboardScanner unconditionally calls Wire.begin(I2C_SDA, I2C_SCL); + -D I2C_SDA=8 + -D I2C_SCL=9 -D INPUTDRIVER_MATRIX_TYPE=1 -D USE_PIN_BUZZER=PIN_BUZZER -D USE_SX127x diff --git a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini index 1d3314aab08..1c2727d1853 100644 --- a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini +++ b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini @@ -19,14 +19,14 @@ custom_meshtastic_support_level = 1 custom_meshtastic_display_name = RAK WisMesh Tap V2 custom_meshtastic_images = rak-wismesh-tap-v2.svg custom_meshtastic_tags = RAK -custom_meshtastic_partition_scheme = 8MB +custom_meshtastic_partition_scheme = 16MB custom_meshtastic_has_mui = true extends = esp32s3_base board = wiscore_rak3312 board_check = true upload_protocol = esptool -board_build.partitions = default_8MB.csv +board_build.partitions = default_16MB.csv build_flags = ${esp32s3_base.build_flags} @@ -37,7 +37,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 [env:rak_wismesh_tap_v2-tft] extends = env:rak_wismesh_tap_v2 diff --git a/variants/esp32s3/rak_wismesh_tap_v2/variant.h b/variants/esp32s3/rak_wismesh_tap_v2/variant.h index 90cb12053cb..4f561b22ddb 100644 --- a/variants/esp32s3/rak_wismesh_tap_v2/variant.h +++ b/variants/esp32s3/rak_wismesh_tap_v2/variant.h @@ -53,6 +53,7 @@ #define BATTERY_PIN 1 #define ADC_CHANNEL ADC1_GPIO1_CHANNEL #define ADC_MULTIPLIER 1.667 +#define OCV_ARRAY 4160, 4020, 3940, 3870, 3810, 3760, 3740, 3720, 3680, 3620, 2990 #define PIN_BUZZER 38 diff --git a/variants/esp32s3/seeed-sensecap-indicator/variant.h b/variants/esp32s3/seeed-sensecap-indicator/variant.h index f946528ae94..281ea2c3dd6 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/variant.h +++ b/variants/esp32s3/seeed-sensecap-indicator/variant.h @@ -67,6 +67,9 @@ #define LORA_DIO1 (3 | IO_EXPANDER) // SX1262 IRQ #define LORA_DIO2 (2 | IO_EXPANDER) // SX1262 BUSY #define LORA_DIO3 +// LORA_DIO1 is an expander pin, not an ESP32 GPIO, so it can't be used as a sleep/GPIO wakeup source +// (shared capability; see its use in sleep.cpp). +#define LORA_DIO1_EXTENDED_IO #define SX126X_CS LORA_CS #define SX126X_DIO1 LORA_DIO1 diff --git a/variants/esp32s3/t-deck/platformio.ini b/variants/esp32s3/t-deck/platformio.ini index a7701549ff3..aba1695cecd 100644 --- a/variants/esp32s3/t-deck/platformio.ini +++ b/variants/esp32s3/t-deck/platformio.ini @@ -29,7 +29,7 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM @@ -55,9 +55,9 @@ build_flags = -D HAS_TFT=1 -D USE_I2S_BUZZER -D RAM_SIZE=5120 - -D LV_LVGL_H_INCLUDE_SIMPLE - -D LV_CONF_INCLUDE_SIMPLE - -D LV_COMP_CONF_INCLUDE_SIMPLE + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE -D LV_USE_SYSMON=0 -D LV_USE_PROFILER=0 -D LV_USE_PERF_MONITOR=0 @@ -72,6 +72,7 @@ build_flags = ; -D CALIBRATE_TOUCH=0 -D LGFX_SCREEN_WIDTH=240 -D LGFX_SCREEN_HEIGHT=320 + -D LGFX_BUFSIZE=153600 -D DISPLAY_SIZE=320x240 ; landscape mode -D LGFX_DRIVER=LGFX_TDECK -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_T_DECK.h\" @@ -79,13 +80,11 @@ build_flags = ; -D GFX_DRIVER_INC=\"graphics/LVGL/LVGL_T_DECK.h\" ; -D LV_USE_ST7789=1 -D VIEW_320x240 -; -D USE_DOUBLE_BUFFER -D USE_PACKET_API -D MAP_FULL_REDRAW - -D CUSTOM_TOUCH_DRIVER +; -D CUSTOM_TOUCH_DRIVER lib_deps = ${env:t-deck.lib_deps} ${device-ui_base.lib_deps} - # renovate: datasource=github-tags depName=bb_captouch packageName=bitbank2/bb_captouch - https://github.com/bitbank2/bb_captouch/archive/refs/tags/1.3.1.zip + ;https://github.com/bitbank2/bb_captouch/archive/refs/tags/1.3.1.zip diff --git a/variants/esp32s3/t-watch-s3/platformio.ini b/variants/esp32s3/t-watch-s3/platformio.ini index 6e7db86301f..ec70148a8d0 100644 --- a/variants/esp32s3/t-watch-s3/platformio.ini +++ b/variants/esp32s3/t-watch-s3/platformio.ini @@ -22,7 +22,7 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini index 15abfadf384..5f243342bb9 100644 --- a/variants/esp32s3/tlora-pager/platformio.ini +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -33,7 +33,7 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM @@ -45,7 +45,7 @@ lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver - https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip + https://github.com/pschatzmann/arduino-audio-driver/archive/v0.3.0.zip # TODO renovate https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip # TODO renovate diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h index d97f864c3e8..c3897a7a412 100644 --- a/variants/esp32s3/tlora-pager/variant.h +++ b/variants/esp32s3/tlora-pager/variant.h @@ -91,6 +91,8 @@ #define EXPANDS_DRV_EN (0) #define EXPANDS_AMP_EN (1) #define EXPANDS_KB_RST (2) +// AudioThread powers the amp on/off around playback via this (opt-in) hook. +#define AUDIO_AMP_ENABLE(on) io.digitalWrite(EXPANDS_AMP_EN, (on) ? HIGH : LOW) #define EXPANDS_LORA_EN (3) #define EXPANDS_GPS_EN (4) #define EXPANDS_NFC_EN (5) diff --git a/variants/esp32s3/tracksenger/platformio.ini b/variants/esp32s3/tracksenger/platformio.ini index 44d07d9e81b..2764a27e8ce 100644 --- a/variants/esp32s3/tracksenger/platformio.ini +++ b/variants/esp32s3/tracksenger/platformio.ini @@ -22,7 +22,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 [env:tracksenger-lcd] custom_meshtastic_hw_model = 48 @@ -48,7 +48,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 [env:tracksenger-oled] custom_meshtastic_hw_model = 48 diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini index 56838b1fc70..3e5f78106b4 100644 --- a/variants/esp32s3/unphone/platformio.ini +++ b/variants/esp32s3/unphone/platformio.ini @@ -37,7 +37,7 @@ build_src_filter = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 # TODO renovate https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0 https://gitlab.com/hamishcunningham/unphonelibrary/-/archive/meshtastic/unphonelibrary-meshtastic.zip diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index 2fa8e865853..5db5a0f8aa5 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -2,7 +2,7 @@ [portduino_base] platform = # renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop - https://github.com/meshtastic/platform-native/archive/cab4b21d902973e43c938dab3cf4844ba02547ec.zip + https://github.com/meshtastic/platform-native/archive/61067ac3774e4fe27aa9762c72cebea507f116c8.zip framework = arduino build_src_filter = @@ -27,7 +27,7 @@ lib_deps = # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.21 + lovyan03/LovyanGFX@1.2.24 ; # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main https://github.com/pine64/libch341-spi-userspace/archive/2e5ff751d0c39667993df672cb683740ed5c9394.zip # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp index 45a64ad3bb5..732ac77fab1 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp @@ -56,8 +56,8 @@ void initVariant() digitalWrite(DHT_POWER, HIGH); pinMode(Battery_POWER, OUTPUT); digitalWrite(Battery_POWER, HIGH); - pinMode(GPS_POWER, OUTPUT); - digitalWrite(GPS_POWER, HIGH); + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, HIGH); } // called from main-nrf52.cpp during the cpuDeepSleep() function @@ -74,12 +74,11 @@ void variant_shutdown() digitalWrite(DHT_POWER, LOW); digitalWrite(ACC_POWER, LOW); digitalWrite(Battery_POWER, LOW); - digitalWrite(GPS_POWER, LOW); // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. for (int pin = 0; pin < 48; pin++) { if (pin == PIN_POWER_USB || pin == BUTTON_PIN || pin == PIN_EN1 || pin == PIN_EN2 || pin == DHT_POWER || - pin == ACC_POWER || pin == Battery_POWER || pin == GPS_POWER || pin == LR1110_SPI_MISO_PIN || + pin == ACC_POWER || pin == Battery_POWER || pin == PIN_GPS_EN || pin == LR1110_SPI_MISO_PIN || pin == LR1110_SPI_MOSI_PIN || pin == LR1110_SPI_SCK_PIN || pin == LR1110_SPI_NSS_PIN || pin == LR1110_BUSY_PIN || pin == LR1110_NRESET_PIN || pin == LR1110_IRQ_PIN || pin == GPS_TX_PIN || pin == GPS_RX_PIN || pin == LED_GREEN || pin == LED_RED || pin == LED_BLUE) { @@ -101,4 +100,4 @@ void variant_shutdown() nrf_gpio_cfg_input(PIN_POWER_USB, NRF_GPIO_PIN_PULLDOWN); // Configure the pin to be woken up as an input nrf_gpio_pin_sense_t sense2 = NRF_GPIO_PIN_SENSE_HIGH; nrf_gpio_cfg_sense_set(PIN_POWER_USB, sense2); -} \ No newline at end of file +} diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h index fa127ae3e09..bd5bae9c858 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h @@ -37,7 +37,8 @@ extern "C" { // Power Pin #define NRF_APM -#define GPS_POWER 14 +#define PIN_GPS_EN 14 +#define GPS_EN_ACTIVE HIGH #define PIN_POWER_USB 31 #define EXT_PWR_DETECT PIN_POWER_USB #define PIN_POWER_DONE 24 diff --git a/variants/nrf52840/heltec_mesh_node_t1/platformio.ini b/variants/nrf52840/heltec_mesh_node_t1/platformio.ini new file mode 100644 index 00000000000..664d1616e0b --- /dev/null +++ b/variants/nrf52840/heltec_mesh_node_t1/platformio.ini @@ -0,0 +1,35 @@ +; Heltec Mesh Node T1 nRF52840/SX1262 device +[env:heltec-mesh-node-t1] +custom_meshtastic_hw_model = 133 +custom_meshtastic_hw_model_slug = HELTEC_MESH_NODE_T1 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Mesh Node T1 +custom_meshtastic_images = heltec-mesh-node-t1.svg +custom_meshtastic_tags = Heltec + +extends = nrf52840_base +board = heltec_mesh_node_t1 +board_level = pr +debug_tool = jlink + +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/heltec_mesh_node_t1 + -DHELTEC_MESH_NODE_T1 + -DUSE_TFTDISPLAY + -DUSER_SETUP_LOADED + -DST7735_DRIVER + -DST7735_REDTAB160x80 + -D TFT_SPI_PORT=SPI1 + -D TFT_CS=ST7735_CS + -D TFT_DC=ST7735_RS + -D TFT_RST=ST7735_RESET + -D TFT_BL=ST7735_BL + -D TFT_BACKLIGHT_ON=LOW + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_node_t1> +lib_deps = + ${nrf52840_base.lib_deps} + lewisxhe/PCF8563_Library@^1.0.1 + bodmer/TFT_eSPI@2.5.43 diff --git a/variants/nrf52840/heltec_mesh_node_t1/variant.cpp b/variants/nrf52840/heltec_mesh_node_t1/variant.cpp new file mode 100644 index 00000000000..1ea214069e6 --- /dev/null +++ b/variants/nrf52840/heltec_mesh_node_t1/variant.cpp @@ -0,0 +1,84 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + pinMode(PIN_BUZZER_VOLTAGE_MULTIPLIER_1, OUTPUT); + pinMode(PIN_BUZZER_VOLTAGE_MULTIPLIER_2, OUTPUT); + digitalWrite(PIN_BUZZER_VOLTAGE_MULTIPLIER_1, HIGH); + digitalWrite(PIN_BUZZER_VOLTAGE_MULTIPLIER_2, HIGH); +} + +void variant_shutdown() +{ + nrf_gpio_cfg_default(ST7735_CS); + nrf_gpio_cfg_default(ST7735_RS); + nrf_gpio_cfg_default(ST7735_SDA); + nrf_gpio_cfg_default(ST7735_SCK); + nrf_gpio_cfg_default(ST7735_RESET); + nrf_gpio_cfg_default(ST7735_BL); + nrf_gpio_cfg_default(VTFT_CTRL); + + nrf_gpio_cfg_default(PIN_WIRE_SDA); + nrf_gpio_cfg_default(PIN_WIRE_SCL); + + nrf_gpio_cfg_default(SX126X_CS); + nrf_gpio_cfg_default(SX126X_DIO1); + nrf_gpio_cfg_default(SX126X_BUSY); + nrf_gpio_cfg_default(SX126X_RESET); + + nrf_gpio_cfg_default(PIN_SPI_MISO); + nrf_gpio_cfg_default(PIN_SPI_MOSI); + nrf_gpio_cfg_default(PIN_SPI_SCK); + + // nrf_gpio_cfg_default(PIN_SPI1_MISO);// ST7735 doesn't support MISO, so we don't configure it at all + nrf_gpio_cfg_default(PIN_SPI1_MOSI); + nrf_gpio_cfg_default(PIN_SPI1_SCK); + + nrf_gpio_cfg_default(PIN_GPS_RESET); + nrf_gpio_cfg_default(PIN_GPS_EN); + nrf_gpio_cfg_default(PIN_GPS_PPS); + nrf_gpio_cfg_default(GPS_TX_PIN); + nrf_gpio_cfg_default(GPS_RX_PIN); + + nrf_gpio_cfg_default(PIN_BUZZER_VOLTAGE_MULTIPLIER_1); + nrf_gpio_cfg_default(PIN_BUZZER_VOLTAGE_MULTIPLIER_2); + + pinMode(PIN_BUZZER, OUTPUT); + digitalWrite(PIN_BUZZER, LOW); + + pinMode(PIN_SENSOR_EN, OUTPUT); + digitalWrite(PIN_SENSOR_EN, !PIN_SENSOR_EN_ACTIVE); // Turn off sensor power + + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, HIGH); +} diff --git a/variants/nrf52840/heltec_mesh_node_t1/variant.h b/variants/nrf52840/heltec_mesh_node_t1/variant.h new file mode 100644 index 00000000000..acf47ca367e --- /dev/null +++ b/variants/nrf52840/heltec_mesh_node_t1/variant.h @@ -0,0 +1,177 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_HELTEC_MESH_NODE_T1_ +#define _VARIANT_HELTEC_MESH_NODE_T1_ +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#define ST7735_CS (0 + 12) +#define ST7735_RS (0 + 22) // DC +#define ST7735_SDA (0 + 24) +#define ST7735_SCK (32 + 0) +#define ST7735_RESET (0 + 20) +#define ST7735_MISO -1 +#define ST7735_BUSY -1 +#define ST7735_BL (0 + 15) +#define VTFT_CTRL (0 + 13) // Active HIGH, powers the ST7735 display +#define SPI_FREQUENCY 80000000 +#define SPI_READ_FREQUENCY 16000000 +#define SCREEN_ROTATE +#define TFT_HEIGHT 160 +#define TFT_WIDTH 80 +#define TFT_OFFSET_X 24 +#define TFT_OFFSET_Y 0 +#define TFT_INVERT false +#define SCREEN_TRANSITION_FRAMERATE 3 // fps +#define DISPLAY_FORCE_SMALL_FONTS + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (0 + 16) +#define LED_BLUE PIN_LED1 // fake for bluefruit library +#define LED_GREEN PIN_LED1 +#define LED_STATE_ON 0 // State when LED is lit + +/* + * Buttons + */ +#define PIN_BUTTON1 (32 + 10) +#define PIN_BUTTON2 (0 + 14) + +/* +No longer populated on PCB +*/ +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) + +/* + * I2C + */ + +#define WIRE_INTERFACES_COUNT 1 + +// I2C bus 1 +#define PIN_WIRE_SDA (32 + 3) +#define PIN_WIRE_SCL (0 + 10) + +#define PIN_SENSOR_EN (32 + 6) // Power control pin for sensors +#define PIN_SENSOR_EN_ACTIVE LOW // Power control active state +// #define ICM_42607P_INT_PIN (32 + 1) // ICM42607P INT1, Arduino pin 33 / nRF P1.01 +// #define ICM_42607P_INT2_PIN (32 + 7) // ICM42607P INT2, Arduino pin 39 / nRF P1.07 + +/* + * Lora radio + */ + +#define USE_SX1262 +#define SX126X_CS (32 + 11) // FIXME - we really should define LORA_CS instead +#define LORA_CS SX126X_CS +#define SX126X_DIO1 (0 + 31) +#define SX126X_BUSY (0 + 29) +#define SX126X_RESET (0 + 2) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +// For LORA, spi 0 +#define PIN_SPI_MISO (0 + 3) +#define PIN_SPI_MOSI (32 + 14) +#define PIN_SPI_SCK (32 + 13) + +#define PIN_SPI1_MISO ST7735_MISO +#define PIN_SPI1_MOSI ST7735_SDA +#define PIN_SPI1_SCK ST7735_SCK + +/* + * GPS pins + */ +#define GPS_UC6580 +#define GPS_BAUDRATE 115200 +#define PIN_GPS_RESET (0 + 26) +#define GPS_RESET_MODE LOW +#define PIN_GPS_EN (0 + 4) +#define GPS_EN_ACTIVE LOW +#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing +#define PIN_GPS_PPS (32 + 9) // Pulse per second input from the GPS +#define GPS_TX_PIN (0 + 7) +#define GPS_RX_PIN (0 + 8) + +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN + +#define PIN_BUZZER (0 + 9) +#define PIN_BUZZER_VOLTAGE_MULTIPLIER_1 (32 + 2) +#define PIN_BUZZER_VOLTAGE_MULTIPLIER_2 (32 + 5) + +#define ADC_CTRL 11 +#define ADC_CTRL_ENABLED HIGH +#define BATTERY_PIN 5 +#define ADC_RESOLUTION 14 + +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (4.916F) + +// nrf52840 AIN3 = Pin 5 +// commented out due to power leakage of 2.9mA in shutdown state see reported issue #8801 +#define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_3 + +// We have AIN3 with a VBAT divider so AIN3 = VBAT * (100/490) +// We have the device going deep sleep under 3.1V, which is AIN3 = 0.63V +// So we can wake up when VBAT>=VDD is restored to 3.3V, where AIN3 = 0.67V +// Ratio 0.67/3.3 = 0.20, so we can pick a bit higher, 2/8 VDD, which means +// VBAT=4.04V +#define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_2_8 + +#define HAS_RTC 0 +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif diff --git a/variants/nrf52840/nrf52.ini b/variants/nrf52840/nrf52.ini index f42c29308fd..e291241a827 100644 --- a/variants/nrf52840/nrf52.ini +++ b/variants/nrf52840/nrf52.ini @@ -2,7 +2,7 @@ ; Instead of the standard nordicnrf52 platform, we use our fork which has our added variant files platform = # renovate: datasource=custom.pio depName=platformio/nordicnrf52 packageName=platformio/platform/nordicnrf52 - platformio/nordicnrf52@10.11.0 + platformio/nordicnrf52@10.12.0 extends = arduino_base platform_packages = ; our custom Git version with C++17 support in platform.txt diff --git a/variants/nrf52840/seeed_mesh_tracker_X1/platformio.ini b/variants/nrf52840/seeed_mesh_tracker_X1/platformio.ini new file mode 100644 index 00000000000..e12fbfd9a7e --- /dev/null +++ b/variants/nrf52840/seeed_mesh_tracker_X1/platformio.ini @@ -0,0 +1,33 @@ +[env:seeed_mesh_tracker_X1] +custom_meshtastic_support_level = 1 +custom_meshtastic_images = seeed-mesh-tracker-x1.svg +custom_meshtastic_tags = Seeed +custom_meshtastic_hw_model = 128 +custom_meshtastic_hw_model_slug = MESH_TRACKER_X1 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_display_name = Seeed SenseCAP Mesh-Tracker-X1 +custom_meshtastic_actively_supported = true + +extends = nrf52840_base +board = mesh-tracker-x1 +board_level = pr +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/seeed_mesh_tracker_X1 + -Isrc/platform/nrf52/softdevice + -Isrc/platform/nrf52/softdevice/nrf52 + -DMESH_TRACKER_X1 + -DCONFIG_NFCT_PINS_AS_GPIOS=1 + -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1 + -DMESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 + -DMESHTASTIC_EXCLUDE_SCREEN=1 + -DMESHTASTIC_EXCLUDE_DETECTIONSENSOR=1 + -DMESHTASTIC_EXCLUDE_WIFI=1 +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_mesh_tracker_X1> +lib_deps = + ${nrf52840_base.lib_deps} + adafruit/Adafruit DRV2605 Library@1.2.4 + +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +upload_protocol = nrfutil diff --git a/variants/nrf52840/seeed_mesh_tracker_X1/rfswitch.h b/variants/nrf52840/seeed_mesh_tracker_X1/rfswitch.h new file mode 100644 index 00000000000..a007b37801e --- /dev/null +++ b/variants/nrf52840/seeed_mesh_tracker_X1/rfswitch.h @@ -0,0 +1,9 @@ +#include "RadioLib.h" +#include "nrf.h" + +static const uint32_t lr2021_rfswitch_dio_pins[] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +static const Module::RfSwitchMode_t lr2021_rfswitch_table[] = { + {LR2021::MODE_STBY, {}}, {LR2021::MODE_RX, {}}, {LR2021::MODE_TX, {}}, + {LR2021::MODE_RX_HF, {}}, {LR2021::MODE_TX_HF, {}}, END_OF_MODE_TABLE, +}; diff --git a/variants/nrf52840/seeed_mesh_tracker_X1/variant.cpp b/variants/nrf52840/seeed_mesh_tracker_X1/variant.cpp new file mode 100644 index 00000000000..15a587e9a87 --- /dev/null +++ b/variants/nrf52840/seeed_mesh_tracker_X1/variant.cpp @@ -0,0 +1,73 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "DebugConfiguration.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); + + pinMode(PIN_BAT_ADC_EN, OUTPUT); + digitalWrite(PIN_BAT_ADC_EN, HIGH); + + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, LOW); + + pinMode(PIN_LED2, OUTPUT); + digitalWrite(PIN_LED2, LOW); + + pinMode(PIN_LED3, OUTPUT); + digitalWrite(PIN_LED3, LOW); + + pinMode(PIN_DRV_EN, OUTPUT); + digitalWrite(PIN_DRV_EN, LOW); + + pinMode(PIN_RTC_EN, OUTPUT); + digitalWrite(PIN_RTC_EN, LOW); + + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); + + pinMode(GPS_VRTC_EN, OUTPUT); + digitalWrite(GPS_VRTC_EN, HIGH); + + pinMode(PIN_GPS_RESET, OUTPUT); + digitalWrite(PIN_GPS_RESET, LOW); + + pinMode(GPS_SLEEP_INT, OUTPUT); + digitalWrite(GPS_SLEEP_INT, HIGH); + + pinMode(GPS_RTC_INT, OUTPUT); + digitalWrite(GPS_RTC_INT, LOW); + + pinMode(PIN_BUZZER, INPUT_PULLDOWN); +} diff --git a/variants/nrf52840/seeed_mesh_tracker_X1/variant.h b/variants/nrf52840/seeed_mesh_tracker_X1/variant.h new file mode 100644 index 00000000000..b9882d71988 --- /dev/null +++ b/variants/nrf52840/seeed_mesh_tracker_X1/variant.h @@ -0,0 +1,172 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_MESH_TRACKER_X1_ +#define _VARIANT_MESH_TRACKER_X1_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// Use the native nrf52 usb power detection +#define NRF_APM + +// PIN_3V3_EN use in deep sleep , as power pin. +#define PIN_3V3_EN (32 + 7) // P1.7, Power to Sensors +#define PIN_BAT_ADC_EN (32 + 6) // P1.6, Power to battery ADC +#define PIN_RTC_EN (0 + 9) // P0.9, Power to RTC + +#define PIN_LED1 (0 + 3) // P0.3, red led +#define PIN_LED2 (0 + 24) // P0.24, green led +#define PIN_LED3 (0 + 28) // P0.28, blue led +#define LED_POWER PIN_LED2 +#define LED_RED PIN_LED1 +#define LED_BLUE -1 // Actually green +#define LED_STATE_ON 1 // State when LED is lit +#define LED_POWER_CRITICAL LED_RED // red LED doubles as the low-battery/critical indicator + +// ARCH_NRF52 auto define HAS_BUTTON 1 +#define HAS_BUTTON 1 +#define BUTTON_PIN (0 + 6) // P0.06 +#define BUTTON_ACTIVE_LOW false +#define BUTTON_ACTIVE_PULLUP false +#define BUTTON_SENSE_TYPE 0x5 // enable input pull-down + +/**************** Sensor *******************/ + +#define HAS_WIRE 1 +#define WIRE_INTERFACES_COUNT 1 // have 1 I2C interface, Wire + +#define PIN_WIRE_SDA (32 + 15) // P1.15 +#define PIN_WIRE_SCL (32 + 14) // P1.14 +#define I2C_NO_RESCAN // I2C is a bit finicky, don't scan too much + +#define HAS_DRV2605 1 // haptic driver +#define DRV2605_USE_LRA +#define PIN_DRV_EN (32 + 5) // P1.05, Power to haptic driver + +#define HAS_SPA06 1 + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (0 + 14) // P0.14 +#define PIN_SERIAL1_TX (0 + 13) // P0.13 + +#define PIN_SERIAL2_RX (0 + 17) // P0.17 +#define PIN_SERIAL2_TX (0 + 16) // P0.16 + +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (32 + 8) // P1.08 +#define PIN_SPI_MOSI (32 + 9) // P1.09 +#define PIN_SPI_SCK (0 + 11) // P0.11 +#define PIN_SPI_NSS (0 + 12) // P0.12 + +#define LORA_RESET (32 + 10) // P1.10 // RST +#define LORA_DIO1 (32 + 1) // P1.01 // IRQ +#define LORA_DIO2 (0 + 7) // P0.07 // BUSY +#define LORA_SCK PIN_SPI_SCK +#define LORA_MISO PIN_SPI_MISO +#define LORA_MOSI PIN_SPI_MOSI +#define LORA_CS PIN_SPI_NSS + +// supported modules list +#define USE_LR2021 +#define IRQ_DIO_NUM 8 + +#define LR2021_IRQ_PIN LORA_DIO1 +#define LR2021_NRESET_PIN LORA_RESET +#define LR2021_BUSY_PIN LORA_DIO2 +#define LR2021_SPI_NSS_PIN LORA_CS +#define LR2021_SPI_SCK_PIN LORA_SCK +#define LR2021_SPI_MOSI_PIN LORA_MOSI +#define LR2021_SPI_MISO_PIN LORA_MISO + +#define LR2021_DIO3_TCXO_VOLTAGE 1.6 +// #define LR2021_DIO_AS_RF_SWITCH + +// GPS +#define HAS_GPS 1 +#define GNSS_AIROHA +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +#define GPS_BAUDRATE 115200 +#define GPS_PROBETRIES 8 + +#define PIN_GPS_EN (32 + 11) // P1.11 +#define GPS_EN_ACTIVE HIGH + +#define PIN_GPS_RESET (0 + 8) // P0.8 +#define GPS_RESET_MODE HIGH + +#define GPS_VRTC_EN (32 + 13) // P1.13, always high +#define GPS_SLEEP_INT (0 + 30) // P0.30, always high +#define GPS_RTC_INT (0 + 29) // P0.29, normal is LOW, wake by HIGH + +#define BATTERY_PIN 2 // P0.02/AIN0, BAT_ADC +#define BATTERY_IMMUTABLE +#define ADC_MULTIPLIER (2.0F) +// P0.04/AIN2 is VCC_ADC, P0.05/AIN3 is CHARGER_DET, P1.03 is CHARGE_STA, P1.04 is CHARGE_DONE + +#define EXT_CHRG_DETECT (32 + 3) // P1.03 +#define EXT_CHRG_DETECT_VALUE LOW +// #define EXT_IS_CHRGD (32 + 4) // P1.04 +// #define EXT_IS_CHRGD_VALUE LOW +#define EXT_PWR_DETECT (0 + 5) // P0.05 + +#define ADC_RESOLUTION 14 +#define BATTERY_SENSE_RESOLUTION_BITS 12 + +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 + +#define OCV_ARRAY 4290, 4118, 4002, 3889, 3798, 3728, 3677, 3643, 3594, 3500, 3100 + +// Buzzer +#define PIN_BUZZER (0 + 25) // P0.25, pwm output + +#define HAS_SCREEN 0 + +#ifdef __cplusplus +} +#endif +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif // _VARIANT_MESH_TRACKER_X1_ diff --git a/variants/nrf52840/seeed_wio_tracker_L1_Pro_1W/platformio.ini b/variants/nrf52840/seeed_wio_tracker_L1_Pro_1W/platformio.ini new file mode 100644 index 00000000000..6093d7a2e2d --- /dev/null +++ b/variants/nrf52840/seeed_wio_tracker_L1_Pro_1W/platformio.ini @@ -0,0 +1,21 @@ +[env:seeed_wio_tracker_L1_Pro_1W] +custom_meshtastic_hw_model = 144 +custom_meshtastic_hw_model_slug = SEEED_WIO_TRACKER_L1_PRO_1W +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Seeed Wio Tracker L1 Pro 1W +custom_meshtastic_images = wio_tracker_l1_case.svg +custom_meshtastic_tags = Seeed +custom_meshtastic_requires_dfu = true + +board = seeed_wio_tracker_L1_Pro_1W +extends = nrf52840_base +build_flags = ${nrf52840_base.build_flags} + -I variants/nrf52840/seeed_wio_tracker_L1_Pro_1W + -D SEEED_WIO_TRACKER_L1_PRO_1W + -I src/platform/nrf52/softdevice + -I src/platform/nrf52/softdevice/nrf52 +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1_Pro_1W> +debug_tool = jlink diff --git a/variants/nrf52840/seeed_wio_tracker_L1_Pro_1W/variant.cpp b/variants/nrf52840/seeed_wio_tracker_L1_Pro_1W/variant.cpp new file mode 100644 index 00000000000..b957db314e8 --- /dev/null +++ b/variants/nrf52840/seeed_wio_tracker_L1_Pro_1W/variant.cpp @@ -0,0 +1,93 @@ +/* + * Digital pin mapping (logical Dx to nRF Port.Pin) and initVariant() for the + * Seeed Wio Tracker L1 Pro 1W. + */ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +/** + * @brief Digital pin to GPIO port/pin mapping table + * + * Format: Logical Pin (Dx) -> nRF Port.Pin (Px.xx) + */ +extern "C" { +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D10 - Peripheral control pins + 41, // D0 P1.09 GNSS_WAKEUP + 7, // D1 P0.07 LORA_DIO1 + 39, // D2 P1.07 LORA_RESET + 42, // D3 P1.10 LORA_BUSY + 46, // D4 P1.14 LORA_CS + 29, // D5 P0.29 (AIN5) LORA_VDET, Pro 1W uses P0.29 not P1.08 + 27, // D6 P0.27 GNSS_TX + 26, // D7 P0.26 GNSS_RX + 30, // D8 P0.30 SPI_SCK + 3, // D9 P0.03 SPI_MISO + 28, // D10 P0.28 SPI_MOSI + + // D11-D12 - LED outputs / Buzzer + 33, // D11 P1.01 Mesh_LED (orange), Pro 1W uses P1.01 not P1.15 + 32, // D12 P1.00 Buzzer, shared with the LED_BLUE macro alias + + // D13 - User input + 8, // D13 P0.08 User Button + + // D14-D15 - OLED I2C0 + 6, // D14 P0.06 OLED SDA + 5, // D15 P0.05 OLED SCL + + // D16 - Battery voltage ADC + 31, // D16 P0.31 VBAT_ADC + + // D17-D18 - Grove I2C1 + 43, // D17 P1.11 GROVE SCL + 44, // D18 P1.12 GROVE SDA + + // D19-D24 - QSPI Flash + 21, // D19 P0.21 QSPI_SCK + 25, // D20 P0.25 QSPI_CSN + 20, // D21 P0.20 QSPI_SIO_0 + 24, // D22 P0.24 QSPI_SIO_1 + 22, // D23 P0.22 QSPI_SIO_2 + 23, // D24 P0.23 QSPI_SIO_3 + + // D25-D29 - Trackball + 36, // D25 TB_UP + 12, // D26 TB_DOWN + 11, // D27 TB_LEFT + 35, // D28 TB_RIGHT + 37, // D29 TB_PRESS + + // D30 - Battery divider enable + 4, // D30 P0.04 BAT_CTL + + // D31-D33 - Pro 1W only + 13, // D31 P0.13 BOOST_EN (Grove 5V Boost) + 47, // D32 P1.15 nRF_Sig_Charge_State (BQ25616 STAT) + 14, // D33 P0.14 LORA_PWR_EN (SX1262 + 1 W PA LDO) +}; +} + +void initVariant() +{ + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + + // Enable battery divider for ADC sampling + pinMode(BAT_READ, OUTPUT); + digitalWrite(BAT_READ, HIGH); + + // Grove 5V Boost: default OFF to save power at boot / shipping state. + // Apps that need Grove 5V can re-enable by writing BOOST_EN_ACTIVE to PIN_BOOST_EN. + pinMode(PIN_BOOST_EN, OUTPUT); + digitalWrite(PIN_BOOST_EN, !BOOST_EN_ACTIVE); + + // LED: default off + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, LOW); + // PIN_LED2 (D12) shares the buzzer pin; ExternalNotification configures it. + // Forcing it LOW here would prevent PWM output. +} diff --git a/variants/nrf52840/seeed_wio_tracker_L1_Pro_1W/variant.h b/variants/nrf52840/seeed_wio_tracker_L1_Pro_1W/variant.h new file mode 100644 index 00000000000..75bfe887b68 --- /dev/null +++ b/variants/nrf52840/seeed_wio_tracker_L1_Pro_1W/variant.h @@ -0,0 +1,201 @@ +#ifndef _SEEED_TRACKER_L1_PRO_1W_H_ +#define _SEEED_TRACKER_L1_PRO_1W_H_ + +#include "WVariant.h" + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Clock Configuration +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define VARIANT_MCK (64000000ul) // Master clock frequency +#define USE_LFXO // 32.768kHz crystal for LFCLK + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Pin Capacity Definitions +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define PINS_COUNT (34u) // Total GPIO pins (D0-D33) +#define NUM_DIGITAL_PINS (34u) // Digital I/O pins +#define NUM_ANALOG_INPUTS (8u) // Analog inputs (A0-A5 + VBAT + AREF) +#define NUM_ANALOG_OUTPUTS (0u) + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// LED Configuration +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Only one real LED (orange, P1.01). PIN_LED2/LED_BLUE/LED_CONN alias D12 (buzzer) for +// ABI compatibility with app code; they drive no hardware LED. +#define PIN_LED1 (11) // Mesh_LED orange P1.01 +#define PIN_LED2 (12) // buzzer pin (no real LED on L1 Pro 1W) + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 +#define LED_STATE_ON 1 // State when LED is lit + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Button Configuration +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define CANCEL_BUTTON_PIN D13 // Program Button +#define CANCEL_BUTTON_ACTIVE_LOW true +#define CANCEL_BUTTON_ACTIVE_PULLUP false + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Digital Pin Mapping (D0-D32) +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// D5 / D11 / D31 / D32 are the Pro 1W V1.0 hardware-revision pins +#define D0 0 // P1.09 GNSS_WAKEUP/IO0 +#define D1 1 // P0.07 LORA_DIO1 +#define D2 2 // P1.07 LORA_RESET +#define D3 3 // P1.10 LORA_BUSY +#define D4 4 // P1.14 LORA_CS +#define D5 5 // P0.29 LORA_VDET (AIN5), replaces the stock L1 LORA_SW on P1.08 +#define D6 6 // P0.27 GNSS_TX +#define D7 7 // P0.26 GNSS_RX +#define D8 8 // P0.30 SPI_SCK +#define D9 9 // P0.03 SPI_MISO +#define D10 10 // P0.28 SPI_MOSI +#define D11 11 // P1.01 Mesh_LED (orange) +#define D12 12 // P1.00 Buzzer +#define D13 13 // P0.08 User Button +#define D14 14 // P0.06 OLED SDA +#define D15 15 // P0.05 OLED SCL +#define D16 16 // P0.31 VBAT_ADC +#define D17 17 // P1.11 Grove I2C1 SCL +#define D18 18 // P1.12 Grove I2C1 SDA +#define D31 31 // P0.13 BOOST_EN (Grove 5V Boost enable), new on Pro 1W +#define D32 32 // P1.15 nRF_Sig_Charge_State (BQ25616 STAT), new on Pro 1W +#define D33 33 // P0.14 LORA_PWR_EN (SX1262 + 1 W PA LDO), new on Pro 1W + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Analog Pin Definitions +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define PIN_A0 0 // P0.02 Analog Input 0 +#define PIN_A1 1 // P0.03 Analog Input 1 +#define PIN_A2 2 // P0.28 Analog Input 2 +#define PIN_A3 3 // P0.29 Analog Input 3 +#define PIN_A4 4 // P0.04 Analog Input 4 +#define PIN_A5 5 // P0.05 Analog Input 5 +#define PIN_VBAT D16 // P0.31 Battery voltage sense + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Communication Interfaces +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// I2C Configuration +#define PIN_WIRE_SDA D14 // P0.06 OLED SDA +#define PIN_WIRE_SCL D15 // P0.05 OLED SCL +#define WIRE_INTERFACES_COUNT 2 +#define PIN_WIRE1_SDA D18 +#define PIN_WIRE1_SCL D17 +#define I2C_NO_RESCAN + +static const uint8_t SDA = PIN_WIRE_SDA; +static const uint8_t SCL = PIN_WIRE_SCL; + +#define HAS_SCREEN 1 +#define USE_SSD1306 1 + +// SPI Configuration (SX1262) +#define SPI_INTERFACES_COUNT 1 +#define PIN_SPI_MISO 9 // P0.03 (D9) +#define PIN_SPI_MOSI 10 // P0.28 (D10) +#define PIN_SPI_SCK 8 // P0.30 (D8) + +// SX1262 LoRa Module Pins +#define USE_SX1262 +#define SX126X_CS D4 // Chip select +#define SX126X_DIO1 D1 // Digital IO 1 (Interrupt) +#define SX126X_BUSY D3 // Busy status +#define SX126X_RESET D2 // Reset control +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 // TCXO supply voltage +#define SX126X_RXEN RADIOLIB_NC +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_DIO2_AS_RF_SWITCH // DIO2 controls antenna switch (no external RXEN/TXEN) + +// SX1262 drives a 1 W external PA; use the fixed PA config, not RadioLib's table. +#define SX126X_NO_POWER_OPTIMIZATION_TABLE + +// Chip-side drive ceiling; limitPower() already subtracted the PA gain. TODO: verify on bench. +#define SX126X_MAX_POWER 22 + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Power Management +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define BAT_READ 30 // D30 = P0.04 Battery divider enable (BAT_CTL) on signal board. +#define ADC_CTRL BAT_READ +#define ADC_CTRL_ENABLED HIGH +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define ADC_MULTIPLIER 2.0 +#define BATTERY_PIN PIN_VBAT +#define AREF_VOLTAGE 3.6 +// We rely on the nrf52840 USB controller to tell us if we are hooked to a power supply +#define NRF_APM + +// BQ25616 single-wire charge status (Pro 1W) +#define PIN_BOOST_EN D31 // D31 / P0.13, Grove 5V Boost enable +#define EXT_CHRG_DETECT D32 // D32 / P1.15, BQ25616 STAT +#define EXT_CHRG_DETECT_VALUE LOW // 0 = charging, 1 = full / charger sleep +#define BOOST_EN_ACTIVE HIGH // HIGH enables Grove 5V Boost + +// External LDO enable for the SX1262 + 1 W PA. D33 rather than raw GPIO 14 because +// g_ADigitalPinMap[14] is D14 (OLED SDA). init() drives it HIGH; deep sleep does not clear it. +#define LORA_PWR_EN D33 +#define SX126X_POWER_EN LORA_PWR_EN + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// GPS L76KB +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define GPS_L76K +#ifdef GPS_L76K +#define GPS_TX_PIN D6 // P0.26 - This is data from the MCU +#define GPS_RX_PIN D7 // P0.27 - This is data from the GNSS +#define HAS_GPS 1 +#define GPS_BAUDRATE 9600 +#define GPS_THREAD_INTERVAL 50 +#define PIN_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN + +#define PIN_GPS_STANDBY D0 +#endif + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// On-board QSPI Flash +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Logical pin indices; the QSPI block is at D19-D24 in variant.cpp. +#define PIN_QSPI_SCK (19) +#define PIN_QSPI_CS (20) +#define PIN_QSPI_IO0 (21) +#define PIN_QSPI_IO1 (22) +#define PIN_QSPI_IO2 (23) +#define PIN_QSPI_IO3 (24) + +#define EXTERNAL_FLASH_DEVICES P25Q16H +#define EXTERNAL_FLASH_USE_QSPI + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Buzzer +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define PIN_BUZZER D12 // P1.00, pwm output + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Trackball +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define CANNED_MESSAGE_ADD_CONFIRMATION 1 + +#define HAS_TRACKBALL 1 +#define TB_UP 25 +#define TB_DOWN 26 +#define TB_LEFT 27 +#define TB_RIGHT 28 +#define TB_PRESS 29 +#define TB_DIRECTION FALLING + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Compatibility Definitions +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#ifdef __cplusplus +extern "C" { +#endif +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) +#ifdef __cplusplus +} +#endif + +#endif // _SEEED_TRACKER_L1_PRO_1W_H_ diff --git a/variants/nrf52840/t-echo-card/platformio.ini b/variants/nrf52840/t-echo-card/platformio.ini index bc012d6e108..4bbd6f41221 100644 --- a/variants/nrf52840/t-echo-card/platformio.ini +++ b/variants/nrf52840/t-echo-card/platformio.ini @@ -6,7 +6,6 @@ debug_tool = jlink build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/t-echo-card - -D PRIVATE_HW -D T_ECHO_CARD build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo-card> diff --git a/variants/nrf52840/t-impulse-plus/platformio.ini b/variants/nrf52840/t-impulse-plus/platformio.ini new file mode 100644 index 00000000000..337b586dc59 --- /dev/null +++ b/variants/nrf52840/t-impulse-plus/platformio.ini @@ -0,0 +1,19 @@ +[env:t-impulse-plus] +custom_meshtastic_hw_model = 135 +custom_meshtastic_hw_model_slug = T_IMPULSE_PLUS +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Impulse Plus +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = true + +extends = nrf52840_base +board = t-impulse-plus +board_level = pr + +build_flags = ${nrf52840_base.build_flags} + -I variants/nrf52840/t-impulse-plus + -D T_IMPULSE_PLUS + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-impulse-plus> diff --git a/variants/nrf52840/t-impulse-plus/variant.cpp b/variants/nrf52840/t-impulse-plus/variant.cpp new file mode 100644 index 00000000000..c50a2527d78 --- /dev/null +++ b/variants/nrf52840/t-impulse-plus/variant.cpp @@ -0,0 +1,91 @@ +/* + * variant.cpp - Digital pin mapping for LilyGo T-Impulse Plus + * + * Board: T-Impulse Plus V1.0 (nRF52840) + * Hardware: + * - SSD1315 OLED + * - SX1262 (S62F) + * - MIA-M10Q GPS + * - ICM20948 IMU + * - ZD25WQ32C Flash + * - TTP223 Touch Button + * - Vibration Motor + */ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +extern "C" { +const uint32_t g_ADigitalPinMap[] = { + // D0-D6: LoRa SX1262 (S62F module) SPI + control + 2, // D0 P0.02 SX1262_RST + 29, // D1 P0.29 SX1262_DIO1 + 31, // D2 P0.31 SX1262_BUSY + 46, // D3 P1.14 SX1262_CS + 3, // D4 P0.03 SPI_SCK + 30, // D5 P0.30 SPI_MISO + 28, // D6 P0.28 SPI_MOSI + + // D7-D8: RF switch control + 45, // D7 P1.13 SX1262_RF_VC1 (TXEN) + 39, // D8 P1.07 SX1262_RF_VC2 (RXEN) + + // D9-D11: GPS (u-blox MIA-M10Q) + 44, // D9 P1.12 GPS_TX (MCU TX -> GPS RX) + 43, // D10 P1.11 GPS_RX (MCU RX <- GPS TX) + 42, // D11 P1.10 GPS_EN + + // D12-D13: Display I2C (SSD1315) + 20, // D12 P0.20 SCREEN_SDA + 15, // D13 P0.15 SCREEN_SCL + + // D14-D15: Sensor I2C (ICM20948, SGM41562) + 40, // D14 P1.08 IMU_SDA + 11, // D15 P0.11 IMU_SCL + + // D16-D17: Battery management + 5, // D16 P0.05 BATTERY_ADC + 25, // D17 P0.25 BATTERY_MEASUREMENT_CONTROL + + // D18: Touch button (TTP223) + 36, // D18 P1.04 TTP223_KEY + + // D19: Vibration motor + 22, // D19 P0.22 VIBRATION_MOTOR + + // D20: LDO enable + 14, // D20 P0.14 RT9080_EN + + // D21-D26: Flash QSPI (ZD25WQ32C) + 12, // D21 P0.12 FLASH_CS + 4, // D22 P0.04 FLASH_SCLK + 6, // D23 P0.06 FLASH_IO0 + 41, // D24 P1.09 FLASH_IO1 + 8, // D25 P0.08 FLASH_IO2 + 26, // D26 P0.26 FLASH_IO3 + + // D27-D28: Interrupt lines + 7, // D27 P0.07 ICM20948_INT + 16, // D28 P0.16 SGM41562_INT + + // D29: Boot button + 24, // D29 P0.24 BOOT +}; +} + +void initVariant() +{ + // Flash CS high (deselect) + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + + // Enable battery voltage measurement + pinMode(BAT_READ, OUTPUT); + digitalWrite(BAT_READ, HIGH); + + // Enable RT9080 LDO + pinMode(D20, OUTPUT); + digitalWrite(D20, HIGH); +} \ No newline at end of file diff --git a/variants/nrf52840/t-impulse-plus/variant.h b/variants/nrf52840/t-impulse-plus/variant.h new file mode 100644 index 00000000000..ff330095685 --- /dev/null +++ b/variants/nrf52840/t-impulse-plus/variant.h @@ -0,0 +1,181 @@ +#ifndef _T_IMPULSE_PLUS_H_ +#define _T_IMPULSE_PLUS_H_ +#include "WVariant.h" + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Clock Configuration +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define VARIANT_MCK (64000000ul) +#define USE_LFXO + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Pin Capacity Definitions +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define PINS_COUNT (30u) +#define NUM_DIGITAL_PINS (30u) +#define NUM_ANALOG_INPUTS (1u) +#define NUM_ANALOG_OUTPUTS (0u) + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Digital Pin Mapping +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define D0 0 // P0.02 SX1262_RST +#define D1 1 // P0.29 SX1262_DIO1 +#define D2 2 // P0.31 SX1262_BUSY +#define D3 3 // P1.14 SX1262_CS +#define D4 4 // P0.03 SPI_SCK +#define D5 5 // P0.30 SPI_MISO +#define D6 6 // P0.28 SPI_MOSI +#define D7 7 // P1.13 RF_VC1 (TXEN) +#define D8 8 // P1.07 RF_VC2 (RXEN) +#define D9 9 // P1.12 GPS module TX → MCU RX +#define D10 10 // P1.11 GPS module RX ← MCU TX +#define D11 11 // P1.10 GPS_EN (active LOW) +#define D12 12 // P0.20 SCREEN_SDA +#define D13 13 // P0.15 SCREEN_SCL +#define D14 14 // P1.08 IMU_SDA +#define D15 15 // P0.11 IMU_SCL +#define D16 16 // P0.05 BATTERY_ADC +#define D17 17 // P0.25 BATTERY_CTL +#define D18 18 // P1.04 TTP223_KEY +#define D19 19 // P0.22 VIBRATION_MOTOR +#define D20 20 // P0.14 RT9080_EN +#define D21 21 // P0.12 FLASH_CS +#define D22 22 // P0.04 FLASH_SCLK +#define D23 23 // P0.06 FLASH_IO0 +#define D24 24 // P1.09 FLASH_IO1 +#define D25 25 // P0.08 FLASH_IO2 +#define D26 26 // P0.26 FLASH_IO3 +#define D27 27 // P0.07 ICM20948_INT +#define D28 28 // P0.16 SGM41562_INT +#define D29 29 // P0.24 BOOT + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// LED Configuration (no physical LED) +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define LED_STATE_ON 1 + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Button Configuration (TTP223 capacitive touch) +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define PIN_BUTTON_TOUCH D18 +#define BUTTON_TOUCH_ACTIVE_LOW true +#define BUTTON_TOUCH_ACTIVE_PULLUP false + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Analog Pin Definitions +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define PIN_VBAT D16 // P0.05 Battery voltage sense + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// I2C Configuration +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Primary I2C: Display (SSD1315) +#define PIN_WIRE_SDA D12 // P0.20 +#define PIN_WIRE_SCL D13 // P0.15 + +// Secondary I2C: IMU (ICM20948) + PMU (SGM41562) +#define WIRE_INTERFACES_COUNT 2 +#define PIN_WIRE1_SDA D14 // P1.08 +#define PIN_WIRE1_SCL D15 // P0.11 +#define I2C_NO_RESCAN + +static const uint8_t SDA = PIN_WIRE_SDA; +static const uint8_t SCL = PIN_WIRE_SCL; + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Display (SSD1315, compatible with SSD1306 driver) +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define HAS_SCREEN 1 +#define USE_SSD1306 1 +#define OLED_TINY +#define OLED_GEOMETRY_OVERRIDE GEOMETRY_64_32 + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// SPI Configuration (SX1262 LoRa) +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define SPI_INTERFACES_COUNT 1 +#define PIN_SPI_SCK D4 // P0.03 +#define PIN_SPI_MISO D5 // P0.30 +#define PIN_SPI_MOSI D6 // P0.28 + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// SX1262 LoRa (S62F module) +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define USE_SX1262 +#define SX126X_CS D3 +#define SX126X_DIO1 D1 +#define SX126X_BUSY D2 +#define SX126X_RESET D0 +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define SX126X_TXEN D7 // RF_VC1 +#define SX126X_RXEN D8 // RF_VC2 + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Power Management +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define BAT_READ D17 // P0.25 Battery measurement control (HIGH = enable) +#define BATTERY_PIN PIN_VBAT +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define ADC_MULTIPLIER 2.0 +#define AREF_VOLTAGE 3.6 + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// GPS (u-blox MIA-M10Q) +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define GPS_UBLOX +#define HAS_GPS 1 +#define GPS_RX_PIN D9 // P1.12 — MCU RX, wired to GPS module TX +#define GPS_TX_PIN D10 // P1.11 — MCU TX, wired to GPS module RX +#define PIN_GPS_EN D11 // P1.10 +#define GPS_EN_ACTIVE LOW +#define GPS_BAUDRATE 38400 +#define GPS_THREAD_INTERVAL 50 +#define PIN_SERIAL1_TX GPS_TX_PIN +#define PIN_SERIAL1_RX GPS_RX_PIN + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// On-board QSPI Flash (ZD25WQ32CEIGR) +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define PIN_QSPI_SCK D22 // P0.04 +#define PIN_QSPI_CS D21 // P0.12 +#define PIN_QSPI_IO0 D23 // P0.06 +#define PIN_QSPI_IO1 D24 // P1.09 +#define PIN_QSPI_IO2 D25 // P0.08 +#define PIN_QSPI_IO3 D26 // P0.26 + +#define EXTERNAL_FLASH_DEVICES W25Q32JV_IQ +#define EXTERNAL_FLASH_USE_QSPI + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Vibration Motor (GPIO active-high) +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define LED_NOTIFICATION D19 // P0.22 +#define HAPTIC_FEEDBACK_PIN LED_NOTIFICATION + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// IMU (ICM20948 on Wire1) +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define HAS_ICM20948 + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Charger (SGM41562 on Wire1 @ 0x03) +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define HAS_SGM41562 +#define SGM41562_WIRE Wire1 + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Compatibility Definitions +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#ifdef __cplusplus +extern "C" { +#endif + +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) + +#ifdef __cplusplus +} +#endif + +#endif // _T_IMPULSE_PLUS_H_ \ No newline at end of file diff --git a/variants/stm32/stm32.ini b/variants/stm32/stm32.ini index d2c15539808..6617507f530 100644 --- a/variants/stm32/stm32.ini +++ b/variants/stm32/stm32.ini @@ -2,7 +2,7 @@ extends = arduino_base platform = # renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32 - platformio/ststm32@19.5.0 + platformio/ststm32@19.7.0 platform_packages = # renovate: datasource=github-tags depName=Arduino_Core_STM32 packageName=stm32duino/Arduino_Core_STM32 platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip diff --git a/version.properties b/version.properties index 56ea393171a..dc88ac471ea 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 24 +build = 27