mirror of
https://github.com/meshtastic/firmware.git
synced 2026-02-03 15:42:02 +00:00
Compare commits
108 Commits
thinknode-
...
m5-shutdow
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de731b8286 | ||
|
|
d8e1553cef | ||
|
|
61147977b2 | ||
|
|
840b08bb9d | ||
|
|
e205d8de0d | ||
|
|
4d9b718374 | ||
|
|
e9d4485bb5 | ||
|
|
7b03980e0a | ||
|
|
004179c045 | ||
|
|
8f630bfcf3 | ||
|
|
7bbfe99fbe | ||
|
|
caae6bc597 | ||
|
|
200e79e800 | ||
|
|
c19fc62683 | ||
|
|
4cf01e7e53 | ||
|
|
ad4b1d9c2b | ||
|
|
68733a6c51 | ||
|
|
22617076f8 | ||
|
|
6f5a7672b4 | ||
|
|
e08c050720 | ||
|
|
28b4f37a93 | ||
|
|
5dd06edd00 | ||
|
|
eeb7373043 | ||
|
|
dbded86dcb | ||
|
|
45fbc0f9d3 | ||
|
|
61b39acc7d | ||
|
|
8af9e7fbdc | ||
|
|
1f7ed6888a | ||
|
|
31bf51b3f2 | ||
|
|
334a4f04cd | ||
|
|
b18742c211 | ||
|
|
03084f6d3b | ||
|
|
94d7b71aa8 | ||
|
|
415686dd06 | ||
|
|
b2f2f6b305 | ||
|
|
df400850c1 | ||
|
|
6ab2f02dbc | ||
|
|
d7d6fe7f0f | ||
|
|
d44ceb6eb2 | ||
|
|
4fd0a8276b | ||
|
|
1d219a93ab | ||
|
|
f710cd6ecb | ||
|
|
571c1ac34c | ||
|
|
fb635987d1 | ||
|
|
a922751afc | ||
|
|
c1e3f56324 | ||
|
|
d0562e1ee6 | ||
|
|
4eb4c4b584 | ||
|
|
69a42e1fd2 | ||
|
|
fd498bebad | ||
|
|
23a8b5a66f | ||
|
|
e1e8d6124d | ||
|
|
10b2eae70c | ||
|
|
cfda9bb8ef | ||
|
|
d1edd386b6 | ||
|
|
b6a1020fc5 | ||
|
|
91ad861b26 | ||
|
|
d54ae5dad8 | ||
|
|
c8079d4115 | ||
|
|
90778a4e78 | ||
|
|
63a97a54e1 | ||
|
|
a2e8e232f1 | ||
|
|
7efc3e3770 | ||
|
|
3d58c6e916 | ||
|
|
c038cfe69a | ||
|
|
0770f25e79 | ||
|
|
8a9830282a | ||
|
|
8894a0b711 | ||
|
|
57a3ff8dfc | ||
|
|
6cff13623f | ||
|
|
b312f226b4 | ||
|
|
b627fa720b | ||
|
|
9faf178bdc | ||
|
|
c98f134b40 | ||
|
|
5838b26d90 | ||
|
|
7221fc4d4b | ||
|
|
a417760887 | ||
|
|
04d2dd3b1c | ||
|
|
6b88d37b73 | ||
|
|
d407ec1975 | ||
|
|
6d6a0734b0 | ||
|
|
0157a769c3 | ||
|
|
73932dd1c3 | ||
|
|
bc2abf3db4 | ||
|
|
073eb2c672 | ||
|
|
4744010295 | ||
|
|
d8d02cd6ec | ||
|
|
3e3299f549 | ||
|
|
eefc08087d | ||
|
|
fb6d199d36 | ||
|
|
fb3bf783dd | ||
|
|
fc268d43d0 | ||
|
|
c38aff7e52 | ||
|
|
7d4600f8c2 | ||
|
|
ff50ba4002 | ||
|
|
5c401b8e34 | ||
|
|
c96ebf15fd | ||
|
|
3e4239daf8 | ||
|
|
ab97c0126f | ||
|
|
d34d694731 | ||
|
|
e545897d4e | ||
|
|
caa6ec0e8a | ||
|
|
49accefd8b | ||
|
|
02f24b9015 | ||
|
|
33ae3777a3 | ||
|
|
021106dfe5 | ||
|
|
afbd9e2180 | ||
|
|
64116cd0d3 |
213
.github/workflows/models_issue_triage.yml
vendored
Normal file
213
.github/workflows/models_issue_triage.yml
vendored
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
name: Issue Triage (Models)
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
models: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
triage:
|
||||||
|
if: ${{ github.repository == 'meshtastic/firmware' && github.event.issue.user.type != 'Bot' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 1: Quality check (spam/AI-slop detection) - runs first, exits early if spam
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
- name: Detect spam or low-quality content
|
||||||
|
uses: actions/ai-inference@v2
|
||||||
|
id: quality
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
max-tokens: 20
|
||||||
|
prompt: |
|
||||||
|
Is this GitHub issue spam, AI-generated slop, or low quality?
|
||||||
|
|
||||||
|
Title: ${{ github.event.issue.title }}
|
||||||
|
Body: ${{ github.event.issue.body }}
|
||||||
|
|
||||||
|
Respond with exactly one of: spam, ai-generated, needs-review, ok
|
||||||
|
system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop.
|
||||||
|
model: openai/gpt-4o-mini
|
||||||
|
|
||||||
|
- name: Apply quality label if needed
|
||||||
|
if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
|
||||||
|
uses: actions/github-script@v8
|
||||||
|
env:
|
||||||
|
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase();
|
||||||
|
const labelMeta = {
|
||||||
|
'spam': { color: 'd73a4a', description: 'Possible spam' },
|
||||||
|
'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' },
|
||||||
|
'needs-review': { color: 'f9d0c4', description: 'Needs human review' },
|
||||||
|
};
|
||||||
|
const meta = labelMeta[label];
|
||||||
|
if (!meta) return;
|
||||||
|
|
||||||
|
// Ensure label exists
|
||||||
|
try {
|
||||||
|
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
||||||
|
} catch (e) {
|
||||||
|
if (e.status !== 404) throw e;
|
||||||
|
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply label
|
||||||
|
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] });
|
||||||
|
|
||||||
|
// Set output to skip remaining steps
|
||||||
|
core.setOutput('is_spam', 'true');
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 2: Duplicate detection - only if not spam
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
- name: Detect duplicate issues
|
||||||
|
if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
|
||||||
|
uses: pelikhan/action-genai-issue-dedup@bdb3b5d9451c1090ffcdf123d7447a5e7c7a2528 # v0.0.19
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 3: Completeness check + auto-labeling (combined into one AI call)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
- name: Determine if completeness check should be skipped
|
||||||
|
if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
|
||||||
|
uses: actions/github-script@v8
|
||||||
|
id: check-skip
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const title = (context.payload.issue.title || '').toLowerCase();
|
||||||
|
const labels = (context.payload.issue.labels || []).map(label => label.name);
|
||||||
|
const hasFeatureRequest = title.includes('feature request');
|
||||||
|
const hasEnhancement = labels.includes('enhancement');
|
||||||
|
const shouldSkip = hasFeatureRequest && hasEnhancement;
|
||||||
|
core.setOutput('should_skip', shouldSkip ? 'true' : 'false');
|
||||||
|
|
||||||
|
- name: Analyze issue completeness and determine labels
|
||||||
|
if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true'
|
||||||
|
uses: actions/ai-inference@v2
|
||||||
|
id: analysis
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
prompt: |
|
||||||
|
Analyze this GitHub issue for completeness and determine if it needs labels.
|
||||||
|
|
||||||
|
IMPORTANT: Distinguish between:
|
||||||
|
- Device/firmware bugs (crashes, reboots, lockups, radio/GPS/display/power issues) - these need device logs
|
||||||
|
- Build/release/packaging issues (missing files, CI failures, download problems) - these do NOT need device logs
|
||||||
|
- Documentation or website issues - these do NOT need device logs
|
||||||
|
|
||||||
|
If this is a device/firmware bug, request device logs and explain how to get them:
|
||||||
|
|
||||||
|
Web Flasher logs:
|
||||||
|
- Go to https://flasher.meshtastic.org
|
||||||
|
- Connect the device via USB and click Connect
|
||||||
|
- Open the device console/log output, reproduce the problem, then copy/download and attach/paste the logs
|
||||||
|
|
||||||
|
Meshtastic CLI logs:
|
||||||
|
- Run: meshtastic --port <serial-port> --noproto
|
||||||
|
- Reproduce the problem, then copy/paste the terminal output
|
||||||
|
|
||||||
|
Also request key context if missing: device model/variant, firmware version, region, steps to reproduce, expected vs actual.
|
||||||
|
|
||||||
|
Respond ONLY with valid JSON (no markdown, no code fences):
|
||||||
|
{"complete": true, "comment": "", "label": "none"}
|
||||||
|
OR
|
||||||
|
{"complete": false, "comment": "Your helpful comment", "label": "needs-logs"}
|
||||||
|
|
||||||
|
Use "needs-logs" ONLY if this is a device/firmware bug AND no logs are attached.
|
||||||
|
Use "needs-info" if basic info like firmware version or steps to reproduce are missing.
|
||||||
|
Use "none" if the issue is complete, is a feature request, or is a build/CI/packaging issue.
|
||||||
|
|
||||||
|
Title: ${{ github.event.issue.title }}
|
||||||
|
Body: ${{ github.event.issue.body }}
|
||||||
|
system-prompt: You are a helpful assistant that triages GitHub issues. Be conservative with labels. Only request device logs for actual device/firmware bugs, not for build/release/CI issues.
|
||||||
|
model: openai/gpt-4o-mini
|
||||||
|
|
||||||
|
- name: Process analysis result
|
||||||
|
if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != ''
|
||||||
|
uses: actions/github-script@v8
|
||||||
|
id: process
|
||||||
|
env:
|
||||||
|
AI_RESPONSE: ${{ steps.analysis.outputs.response }}
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
let raw = (process.env.AI_RESPONSE || '').trim();
|
||||||
|
|
||||||
|
// Strip markdown code fences if present
|
||||||
|
raw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
|
||||||
|
|
||||||
|
let complete = true;
|
||||||
|
let comment = '';
|
||||||
|
let label = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
complete = !!parsed.complete;
|
||||||
|
comment = (parsed.comment ?? '').toString().trim();
|
||||||
|
label = (parsed.label ?? 'none').toString().trim().toLowerCase();
|
||||||
|
} catch {
|
||||||
|
// If JSON parse fails, log warning and don't comment (avoid posting raw JSON)
|
||||||
|
console.log('Failed to parse AI response as JSON:', raw);
|
||||||
|
complete = true;
|
||||||
|
comment = '';
|
||||||
|
label = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate label
|
||||||
|
const allowedLabels = new Set(['needs-logs', 'needs-info', 'none']);
|
||||||
|
if (!allowedLabels.has(label)) label = 'none';
|
||||||
|
|
||||||
|
// Only comment if we have a valid parsed comment (not raw JSON)
|
||||||
|
const shouldComment = !complete && comment.length > 0 && !comment.startsWith('{');
|
||||||
|
core.setOutput('should_comment', shouldComment ? 'true' : 'false');
|
||||||
|
core.setOutput('comment_body', comment);
|
||||||
|
core.setOutput('label', label);
|
||||||
|
|
||||||
|
- name: Apply triage label
|
||||||
|
if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none'
|
||||||
|
uses: actions/github-script@v8
|
||||||
|
env:
|
||||||
|
LABEL_NAME: ${{ steps.process.outputs.label }}
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const label = process.env.LABEL_NAME;
|
||||||
|
const labelMeta = {
|
||||||
|
'needs-logs': { color: 'cfd3d7', description: 'Device logs requested for triage' },
|
||||||
|
'needs-info': { color: 'f9d0c4', description: 'More information requested for triage' },
|
||||||
|
};
|
||||||
|
const meta = labelMeta[label];
|
||||||
|
if (!meta) return;
|
||||||
|
|
||||||
|
// Ensure label exists
|
||||||
|
try {
|
||||||
|
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
||||||
|
} catch (e) {
|
||||||
|
if (e.status !== 404) throw e;
|
||||||
|
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply label
|
||||||
|
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] });
|
||||||
|
|
||||||
|
- name: Comment on issue
|
||||||
|
if: steps.process.outputs.should_comment == 'true'
|
||||||
|
uses: actions/github-script@v8
|
||||||
|
env:
|
||||||
|
COMMENT_BODY: ${{ steps.process.outputs.comment_body }}
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.payload.issue.number,
|
||||||
|
body: process.env.COMMENT_BODY
|
||||||
|
});
|
||||||
139
.github/workflows/models_pr_triage.yml
vendored
Normal file
139
.github/workflows/models_pr_triage.yml
vendored
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
name: PR Triage (Models)
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
models: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
triage:
|
||||||
|
if: ${{ github.repository == 'meshtastic/firmware' && github.event.pull_request.user.type != 'Bot' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 1: Check if PR already has automation/type labels (skip if so)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
- name: Check existing labels
|
||||||
|
uses: actions/github-script@v8
|
||||||
|
id: check-labels
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const skipLabels = new Set(['automation']);
|
||||||
|
const typeLabels = new Set(['bugfix', 'hardware-support', 'enhancement', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']);
|
||||||
|
const prLabels = context.payload.pull_request.labels.map(l => l.name);
|
||||||
|
|
||||||
|
const shouldSkipAll = prLabels.some(l => skipLabels.has(l));
|
||||||
|
const hasTypeLabel = prLabels.some(l => typeLabels.has(l));
|
||||||
|
|
||||||
|
core.setOutput('skip_all', shouldSkipAll ? 'true' : 'false');
|
||||||
|
core.setOutput('has_type_label', hasTypeLabel ? 'true' : 'false');
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 2: Quality check (spam/AI-slop detection)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
- name: Detect spam or low-quality content
|
||||||
|
if: steps.check-labels.outputs.skip_all != 'true'
|
||||||
|
uses: actions/ai-inference@v2
|
||||||
|
id: quality
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
max-tokens: 20
|
||||||
|
prompt: |
|
||||||
|
Is this GitHub pull request spam, AI-generated slop, or low quality?
|
||||||
|
|
||||||
|
Title: ${{ github.event.pull_request.title }}
|
||||||
|
Body: ${{ github.event.pull_request.body }}
|
||||||
|
|
||||||
|
Respond with exactly one of: spam, ai-generated, needs-review, ok
|
||||||
|
system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop.
|
||||||
|
model: openai/gpt-4o-mini
|
||||||
|
|
||||||
|
- name: Apply quality label if needed
|
||||||
|
if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
|
||||||
|
uses: actions/github-script@v8
|
||||||
|
id: quality-label
|
||||||
|
env:
|
||||||
|
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase();
|
||||||
|
const labelMeta = {
|
||||||
|
'spam': { color: 'd73a4a', description: 'Possible spam' },
|
||||||
|
'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' },
|
||||||
|
'needs-review': { color: 'f9d0c4', description: 'Needs human review' },
|
||||||
|
};
|
||||||
|
const meta = labelMeta[label];
|
||||||
|
if (!meta) return;
|
||||||
|
|
||||||
|
// Ensure label exists
|
||||||
|
try {
|
||||||
|
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
||||||
|
} catch (e) {
|
||||||
|
if (e.status !== 404) throw e;
|
||||||
|
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply label
|
||||||
|
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] });
|
||||||
|
|
||||||
|
core.setOutput('is_spam', 'true');
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 3: Auto-label PR type (bugfix/hardware-support/enhancement)
|
||||||
|
# Only skip for spam/ai-generated; still classify needs-review PRs
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
- name: Classify PR for labeling
|
||||||
|
if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.quality.outputs.response != 'spam' && steps.quality.outputs.response != 'ai-generated'
|
||||||
|
uses: actions/ai-inference@v2
|
||||||
|
id: classify
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
max-tokens: 30
|
||||||
|
prompt: |
|
||||||
|
Classify this pull request into exactly one category.
|
||||||
|
|
||||||
|
Return exactly one of: bugfix, hardware-support, enhancement
|
||||||
|
|
||||||
|
Use bugfix if it fixes a bug, crash, or incorrect behavior.
|
||||||
|
Use hardware-support if it adds or improves support for a specific hardware device/variant.
|
||||||
|
Use enhancement if it adds a new feature, improves performance, or refactors code.
|
||||||
|
|
||||||
|
Title: ${{ github.event.pull_request.title }}
|
||||||
|
Body: ${{ github.event.pull_request.body }}
|
||||||
|
system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label.
|
||||||
|
model: openai/gpt-4o-mini
|
||||||
|
|
||||||
|
- name: Apply type label
|
||||||
|
if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != ''
|
||||||
|
uses: actions/github-script@v8
|
||||||
|
env:
|
||||||
|
TYPE_LABEL: ${{ steps.classify.outputs.response }}
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const label = (process.env.TYPE_LABEL || '').trim().toLowerCase();
|
||||||
|
const labelMeta = {
|
||||||
|
'bugfix': { color: 'd73a4a', description: 'Bug fix' },
|
||||||
|
'hardware-support': { color: '0e8a16', description: 'Hardware support addition or improvement' },
|
||||||
|
'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' },
|
||||||
|
};
|
||||||
|
const meta = labelMeta[label];
|
||||||
|
if (!meta) return;
|
||||||
|
|
||||||
|
// Ensure label exists
|
||||||
|
try {
|
||||||
|
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
||||||
|
} catch (e) {
|
||||||
|
if (e.status !== 404) throw e;
|
||||||
|
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply label
|
||||||
|
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] });
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -50,3 +50,6 @@ idf_component.yml
|
|||||||
CMakeLists.txt
|
CMakeLists.txt
|
||||||
/sdkconfig.*
|
/sdkconfig.*
|
||||||
.dummy/*
|
.dummy/*
|
||||||
|
|
||||||
|
# PYTHONPATH used by the Nix shell
|
||||||
|
.python3
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ plugins:
|
|||||||
lint:
|
lint:
|
||||||
enabled:
|
enabled:
|
||||||
- checkov@3.2.497
|
- checkov@3.2.497
|
||||||
- renovate@42.81.8
|
- renovate@42.84.2
|
||||||
- prettier@3.8.0
|
- prettier@3.8.0
|
||||||
- trufflehog@3.92.4
|
- trufflehog@3.92.5
|
||||||
- yamllint@1.38.0
|
- yamllint@1.38.0
|
||||||
- bandit@1.9.2
|
- bandit@1.9.3
|
||||||
- trivy@0.68.2
|
- trivy@0.68.2
|
||||||
- taplo@0.10.0
|
- taplo@0.10.0
|
||||||
- ruff@0.14.11
|
- ruff@0.14.13
|
||||||
- isort@7.0.0
|
- isort@7.0.0
|
||||||
- markdownlint@0.47.0
|
- markdownlint@0.47.0
|
||||||
- oxipng@10.0.0
|
- oxipng@10.0.0
|
||||||
@@ -26,7 +26,7 @@ lint:
|
|||||||
- hadolint@2.14.0
|
- hadolint@2.14.0
|
||||||
- shfmt@3.6.0
|
- shfmt@3.6.0
|
||||||
- shellcheck@0.11.0
|
- shellcheck@0.11.0
|
||||||
- black@25.12.0
|
- black@26.1.0
|
||||||
- git-diff-check
|
- git-diff-check
|
||||||
- gitleaks@8.30.0
|
- gitleaks@8.30.0
|
||||||
- clang-format@16.0.3
|
- clang-format@16.0.3
|
||||||
|
|||||||
23
bin/config.d/lora-usb-umesh-1262-30dbm.yaml
Normal file
23
bin/config.d/lora-usb-umesh-1262-30dbm.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Lora:
|
||||||
|
Module: sx1262
|
||||||
|
CS: 0
|
||||||
|
IRQ: 6
|
||||||
|
Reset: 1
|
||||||
|
Busy: 4
|
||||||
|
RXen: 2
|
||||||
|
DIO2_AS_RF_SWITCH: true
|
||||||
|
spidev: ch341
|
||||||
|
USB_PID: 0x5512
|
||||||
|
USB_VID: 0x1A86
|
||||||
|
DIO3_TCXO_VOLTAGE: true
|
||||||
|
# USB_Serialnum: 12345678
|
||||||
|
SX126X_MAX_POWER: 22
|
||||||
|
# Reduce output power to improve EMI
|
||||||
|
NUM_PA_POINTS: 22
|
||||||
|
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
|
||||||
|
# Note: This module integrates an additional PA to achieve higher output power.
|
||||||
|
# The 'power' parameter here does not represent the actual RF output.
|
||||||
|
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
|
||||||
|
# Each array element corresponds to the additional gain when that input level is set,
|
||||||
|
# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index].
|
||||||
|
# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information.
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
Lora:
|
|
||||||
Module: sx1262
|
|
||||||
CS: 0
|
|
||||||
IRQ: 6
|
|
||||||
Reset: 1
|
|
||||||
Busy: 4
|
|
||||||
RXen: 2
|
|
||||||
DIO2_AS_RF_SWITCH: true
|
|
||||||
spidev: ch341
|
|
||||||
USB_PID: 0x5512
|
|
||||||
USB_VID: 0x1A86
|
|
||||||
DIO3_TCXO_VOLTAGE: true
|
|
||||||
# USB_Serialnum: 12345678
|
|
||||||
SX126X_MAX_POWER: 30
|
|
||||||
# Reduce output power to improve EMI
|
|
||||||
23
bin/config.d/lora-usb-umesh-1268-30dbm.yaml
Normal file
23
bin/config.d/lora-usb-umesh-1268-30dbm.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Lora:
|
||||||
|
Module: sx1268
|
||||||
|
CS: 0
|
||||||
|
IRQ: 6
|
||||||
|
Reset: 1
|
||||||
|
Busy: 4
|
||||||
|
RXen: 2
|
||||||
|
DIO2_AS_RF_SWITCH: true
|
||||||
|
spidev: ch341
|
||||||
|
USB_PID: 0x5512
|
||||||
|
USB_VID: 0x1A86
|
||||||
|
DIO3_TCXO_VOLTAGE: true
|
||||||
|
# USB_Serialnum: 12345678
|
||||||
|
SX126X_MAX_POWER: 22
|
||||||
|
# Reduce output power to improve EMI
|
||||||
|
NUM_PA_POINTS: 22
|
||||||
|
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
|
||||||
|
# Note: This module integrates an additional PA to achieve higher output power.
|
||||||
|
# The 'power' parameter here does not represent the actual RF output.
|
||||||
|
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
|
||||||
|
# Each array element corresponds to the additional gain when that input level is set,
|
||||||
|
# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index].
|
||||||
|
# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information.
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
Lora:
|
|
||||||
Module: sx1268
|
|
||||||
CS: 0
|
|
||||||
IRQ: 6
|
|
||||||
Reset: 1
|
|
||||||
Busy: 4
|
|
||||||
RXen: 2
|
|
||||||
DIO2_AS_RF_SWITCH: true
|
|
||||||
spidev: ch341
|
|
||||||
USB_PID: 0x5512
|
|
||||||
USB_VID: 0x1A86
|
|
||||||
DIO3_TCXO_VOLTAGE: true
|
|
||||||
# USB_Serialnum: 12345678
|
|
||||||
SX126X_MAX_POWER: 30
|
|
||||||
# Reduce output power to improve EMI
|
|
||||||
@@ -156,16 +156,8 @@ IF %BPS_RESET% EQU 1 (
|
|||||||
SET "PROGNAME=!FILENAME:.factory.bin=!"
|
SET "PROGNAME=!FILENAME:.factory.bin=!"
|
||||||
CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
|
CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
|
||||||
|
|
||||||
IF "__!MCU!__" == "__esp32s3__" (
|
@REM Determine OTA filename based on MCU type (unified OTA format)
|
||||||
@REM We are working with ESP32-S3
|
SET "OTA_FILENAME=mt-!MCU!-ota.bin"
|
||||||
SET "OTA_FILENAME=bleota-s3.bin"
|
|
||||||
) ELSE IF "__!MCU!__" == "__esp32c3__" (
|
|
||||||
@REM We are working with ESP32-C3
|
|
||||||
SET "OTA_FILENAME=bleota-c3.bin"
|
|
||||||
) ELSE (
|
|
||||||
@REM Everything else
|
|
||||||
SET "OTA_FILENAME=bleota.bin"
|
|
||||||
)
|
|
||||||
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
|
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
|
||||||
|
|
||||||
@REM Set SPIFFS filename with "littlefs-" prefix.
|
@REM Set SPIFFS filename with "littlefs-" prefix.
|
||||||
|
|||||||
@@ -32,6 +32,19 @@ if ! command -v jq >/dev/null 2>&1; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# esptool v5 supports commands with dashes and deprecates commands with
|
||||||
|
# underscores. Prior versions only support commands with underscores
|
||||||
|
if ${ESPTOOL_CMD} | grep --quiet write-flash
|
||||||
|
then
|
||||||
|
ESPTOOL_WRITE_FLASH=write-flash
|
||||||
|
ESPTOOL_ERASE_FLASH=erase-flash
|
||||||
|
ESPTOOL_READ_FLASH_STATUS=read-flash-status
|
||||||
|
else
|
||||||
|
ESPTOOL_WRITE_FLASH=write_flash
|
||||||
|
ESPTOOL_ERASE_FLASH=erase_flash
|
||||||
|
ESPTOOL_READ_FLASH_STATUS=read_flash_status
|
||||||
|
fi
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Usage info
|
# Usage info
|
||||||
@@ -83,8 +96,8 @@ while [ $# -gt 0 ]; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [[ $BPS_RESET == true ]]; then
|
if [[ $BPS_RESET == true ]]; then
|
||||||
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status
|
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset ${ESPTOOL_READ_FLASH_STATUS}
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[ -z "$FILENAME" ] && [ -n "$1" ] && {
|
[ -z "$FILENAME" ] && [ -n "$1" ] && {
|
||||||
@@ -118,14 +131,8 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Determine OTA filename based on MCU type
|
# Determine OTA filename based on MCU type (unified OTA format)
|
||||||
if [ "$MCU" == "esp32s3" ]; then
|
OTAFILE="mt-${MCU}-ota.bin"
|
||||||
OTAFILE=bleota-s3.bin
|
|
||||||
elif [ "$MCU" == "esp32c3" ]; then
|
|
||||||
OTAFILE=bleota-c3.bin
|
|
||||||
else
|
|
||||||
OTAFILE=bleota.bin
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set SPIFFS filename with "littlefs-" prefix.
|
# Set SPIFFS filename with "littlefs-" prefix.
|
||||||
SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"
|
SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"
|
||||||
@@ -144,12 +151,12 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
|
echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
|
||||||
$ESPTOOL_CMD erase-flash
|
$ESPTOOL_CMD ${ESPTOOL_ERASE_FLASH}
|
||||||
$ESPTOOL_CMD write-flash $FIRMWARE_OFFSET "${FILENAME}"
|
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $FIRMWARE_OFFSET "${FILENAME}"
|
||||||
echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}"
|
echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}"
|
||||||
$ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}"
|
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OTA_OFFSET "${OTAFILE}"
|
||||||
echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}"
|
echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}"
|
||||||
$ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}"
|
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OFFSET "${SPIFFSFILE}"
|
||||||
|
|
||||||
else
|
else
|
||||||
show_help
|
show_help
|
||||||
|
|||||||
@@ -20,6 +20,17 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# esptool v5 supports commands with dashes and deprecates commands with
|
||||||
|
# underscores. Prior versions only support commands with underscores
|
||||||
|
if ${ESPTOOL_CMD} | grep --quiet write-flash
|
||||||
|
then
|
||||||
|
ESPTOOL_WRITE_FLASH=write-flash
|
||||||
|
ESPTOOL_READ_FLASH_STATUS=read-flash-status
|
||||||
|
else
|
||||||
|
ESPTOOL_WRITE_FLASH=write_flash
|
||||||
|
ESPTOOL_READ_FLASH_STATUS=read_flash_status
|
||||||
|
fi
|
||||||
|
|
||||||
# Usage info
|
# Usage info
|
||||||
show_help() {
|
show_help() {
|
||||||
cat << EOF
|
cat << EOF
|
||||||
@@ -69,7 +80,7 @@ done
|
|||||||
shift "$((OPTIND-1))"
|
shift "$((OPTIND-1))"
|
||||||
|
|
||||||
if [ "$CHANGE_MODE" = true ]; then
|
if [ "$CHANGE_MODE" = true ]; then
|
||||||
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status
|
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset ${ESPTOOL_READ_FLASH_STATUS}
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -80,7 +91,7 @@ fi
|
|||||||
|
|
||||||
if [[ -f "$FILENAME" && "$FILENAME" != *.factory.bin ]]; then
|
if [[ -f "$FILENAME" && "$FILENAME" != *.factory.bin ]]; then
|
||||||
echo "Trying to flash update ${FILENAME}"
|
echo "Trying to flash update ${FILENAME}"
|
||||||
$ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}"
|
$ESPTOOL_CMD --baud $FLASH_BAUD ${ESPTOOL_WRITE_FLASH} $UPDATE_OFFSET "${FILENAME}"
|
||||||
else
|
else
|
||||||
show_help
|
show_help
|
||||||
echo "Invalid file: ${FILENAME}"
|
echo "Invalid file: ${FILENAME}"
|
||||||
|
|||||||
@@ -87,6 +87,9 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="2.7.19" date="2026-01-22">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.19</url>
|
||||||
|
</release>
|
||||||
<release version="2.7.18" date="2026-01-02">
|
<release version="2.7.18" date="2026-01-02">
|
||||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.18</url>
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.18</url>
|
||||||
</release>
|
</release>
|
||||||
|
|||||||
50
boards/minimesh_lite.json
Normal file
50
boards/minimesh_lite.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"arduino": {
|
||||||
|
"ldscript": "nrf52840_s140_v6.ld"
|
||||||
|
},
|
||||||
|
"core": "nRF5",
|
||||||
|
"cpu": "cortex-m4",
|
||||||
|
"extra_flags": "-DMINIMESH_LITE -DNRF52840_XXAA",
|
||||||
|
"f_cpu": "64000000L",
|
||||||
|
"hwids": [
|
||||||
|
["0x239A", "0x8029"],
|
||||||
|
["0x239A", "0x0029"],
|
||||||
|
["0x239A", "0x002A"]
|
||||||
|
],
|
||||||
|
"usb_product": "Minimesh Lite",
|
||||||
|
"mcu": "nrf52840",
|
||||||
|
"variant": "dls_Minimesh_Lite",
|
||||||
|
"bsp": {
|
||||||
|
"name": "adafruit"
|
||||||
|
},
|
||||||
|
"softdevice": {
|
||||||
|
"sd_flags": "-DS140",
|
||||||
|
"sd_name": "s140",
|
||||||
|
"sd_version": "6.1.1",
|
||||||
|
"sd_fwid": "0x00B6"
|
||||||
|
},
|
||||||
|
"bootloader": {
|
||||||
|
"settings_addr": "0xFF000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connectivity": ["bluetooth"],
|
||||||
|
"debug": {
|
||||||
|
"jlink_device": "nRF52840_xxAA",
|
||||||
|
"svd_path": "nrf52840.svd"
|
||||||
|
},
|
||||||
|
"frameworks": ["arduino"],
|
||||||
|
"name": "Minimesh Lite",
|
||||||
|
"upload": {
|
||||||
|
"maximum_ram_size": 248832,
|
||||||
|
"maximum_size": 815104,
|
||||||
|
"speed": 115200,
|
||||||
|
"protocol": "nrfutil",
|
||||||
|
"protocols": ["nrfutil", "jlink", "nrfjprog", "stlink"],
|
||||||
|
"use_1200bps_touch": true,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"wait_for_upload_port": true
|
||||||
|
},
|
||||||
|
"url": "https://deeplabstudio.com",
|
||||||
|
"vendor": "Deeplab Studio"
|
||||||
|
}
|
||||||
6
debian/changelog
vendored
6
debian/changelog
vendored
@@ -1,3 +1,9 @@
|
|||||||
|
meshtasticd (2.7.19.0) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Version 2.7.19
|
||||||
|
|
||||||
|
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Thu, 22 Jan 2026 22:17:40 +0000
|
||||||
|
|
||||||
meshtasticd (2.7.18.0) unstable; urgency=medium
|
meshtasticd (2.7.18.0) unstable; urgency=medium
|
||||||
|
|
||||||
* Version 2.7.18
|
* Version 2.7.18
|
||||||
|
|||||||
44
flake.lock
generated
Normal file
44
flake.lock
generated
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1767039857,
|
||||||
|
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1766314097,
|
||||||
|
"narHash": "sha256-laJftWbghBehazn/zxVJ8NdENVgjccsWAdAqKXhErrM=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "306ea70f9eb0fb4e040f8540e2deab32ed7e2055",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
66
flake.nix
Normal file
66
flake.nix
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
description = "Nix flake to compile Meshtastic firmware";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
|
||||||
|
# Shim to make flake.nix work with stable Nix.
|
||||||
|
flake-compat = {
|
||||||
|
url = "github:NixOS/flake-compat";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
inputs:
|
||||||
|
let
|
||||||
|
lib = inputs.nixpkgs.lib;
|
||||||
|
|
||||||
|
forAllSystems =
|
||||||
|
fn:
|
||||||
|
lib.genAttrs lib.systems.flakeExposed (
|
||||||
|
system:
|
||||||
|
fn {
|
||||||
|
pkgs = import inputs.nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
};
|
||||||
|
inherit system;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells = forAllSystems (
|
||||||
|
{ pkgs, ... }:
|
||||||
|
let
|
||||||
|
python3 = pkgs.python312.withPackages (
|
||||||
|
ps: with ps; [
|
||||||
|
google
|
||||||
|
]
|
||||||
|
);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
python3
|
||||||
|
platformio
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
# Set up PlatformIO to use a local core directory.
|
||||||
|
export PLATFORMIO_CORE_DIR=$PWD/.platformio
|
||||||
|
# Tell pip to put packages into $PIP_PREFIX instead of the usual
|
||||||
|
# location. This is especially necessary under NixOS to avoid having
|
||||||
|
# pip trying to write to the read-only Nix store. For more info,
|
||||||
|
# see https://wiki.nixos.org/wiki/Python
|
||||||
|
export PIP_PREFIX=$PWD/.python3
|
||||||
|
export PYTHONPATH="$PIP_PREFIX/${python3.sitePackages}"
|
||||||
|
export PATH="$PIP_PREFIX/bin:$PATH"
|
||||||
|
# Avoids reproducibility issues with some Python packages
|
||||||
|
# See https://nixos.org/manual/nixpkgs/stable/#python-setup.py-bdist_wheel-cannot-create-.whl
|
||||||
|
unset SOURCE_DATE_EPOCH
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -43,13 +43,11 @@ class Esp32C3ExceptionDecoder(DeviceMonitorFilterBase):
|
|||||||
self.enabled = self.setup_paths()
|
self.enabled = self.setup_paths()
|
||||||
|
|
||||||
if self.config.get("env:" + self.environment, "build_type") != "debug":
|
if self.config.get("env:" + self.environment, "build_type") != "debug":
|
||||||
print(
|
print("""
|
||||||
"""
|
|
||||||
Please build project in debug configuration to get more details about an exception.
|
Please build project in debug configuration to get more details about an exception.
|
||||||
See https://docs.platformio.org/page/projectconf/build_configurations.html
|
See https://docs.platformio.org/page/projectconf/build_configurations.html
|
||||||
|
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ build_flags = -Wno-missing-field-initializers
|
|||||||
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
||||||
-DMESHTASTIC_EXCLUDE_POWERMON=1
|
-DMESHTASTIC_EXCLUDE_POWERMON=1
|
||||||
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
|
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
|
||||||
|
-DLED_BUILTIN=-1
|
||||||
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
|
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
|
||||||
#-D OLED_PL=1
|
#-D OLED_PL=1
|
||||||
#-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs
|
#-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs
|
||||||
@@ -119,7 +120,7 @@ lib_deps =
|
|||||||
[device-ui_base]
|
[device-ui_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||||
https://github.com/meshtastic/device-ui/archive/5a870c623a4e9ab7a7abe3d02950536f107d1a31.zip
|
https://github.com/meshtastic/device-ui/archive/63967a4a557d33d56fc5746f9128200dde2d88c5.zip
|
||||||
|
|
||||||
; Common libs for environmental measurements in telemetry module
|
; Common libs for environmental measurements in telemetry module
|
||||||
[environmental_base]
|
[environmental_base]
|
||||||
@@ -212,3 +213,30 @@ lib_deps =
|
|||||||
sensirion/Sensirion Core@0.7.2
|
sensirion/Sensirion Core@0.7.2
|
||||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
||||||
sensirion/Sensirion I2C SCD4x@1.1.0
|
sensirion/Sensirion I2C SCD4x@1.1.0
|
||||||
|
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
|
||||||
|
[environmental_extra_no_bsec]
|
||||||
|
lib_deps =
|
||||||
|
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
|
||||||
|
adafruit/Adafruit BMP3XX Library@2.1.6
|
||||||
|
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
|
||||||
|
adafruit/Adafruit MAX1704X@1.0.3
|
||||||
|
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
|
||||||
|
adafruit/Adafruit SHTC3 Library@1.0.2
|
||||||
|
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
|
||||||
|
adafruit/Adafruit LPS2X@2.0.6
|
||||||
|
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library
|
||||||
|
adafruit/Adafruit SHT31 Library@2.2.2
|
||||||
|
# renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library
|
||||||
|
adafruit/Adafruit VEML7700 Library@2.1.6
|
||||||
|
# renovate: datasource=custom.pio depName=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library
|
||||||
|
adafruit/Adafruit SHT4x Library@1.0.5
|
||||||
|
# renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library
|
||||||
|
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
|
||||||
|
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
|
||||||
|
closedcube/ClosedCube OPT3001@1.1.2
|
||||||
|
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
|
||||||
|
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
|
||||||
|
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
|
||||||
|
sensirion/Sensirion Core@0.7.2
|
||||||
|
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
||||||
|
sensirion/Sensirion I2C SCD4x@1.1.0
|
||||||
Submodule protobufs updated: 4b9f104a18...bc63a57f9e
12
shell.nix
Normal file
12
shell.nix
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
(import (
|
||||||
|
let
|
||||||
|
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||||
|
nodeName = lock.nodes.root.inputs.flake-compat;
|
||||||
|
in
|
||||||
|
fetchTarball {
|
||||||
|
url =
|
||||||
|
lock.nodes.${nodeName}.locked.url
|
||||||
|
or "https://github.com/NixOS/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz";
|
||||||
|
sha256 = lock.nodes.${nodeName}.locked.narHash;
|
||||||
|
}
|
||||||
|
) { src = ./.; }).shellNix
|
||||||
207
src/Power.cpp
207
src/Power.cpp
@@ -1,11 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* @file Power.cpp
|
* @file Power.cpp
|
||||||
* @brief This file contains the implementation of the Power class, which is responsible for managing power-related functionality
|
* @brief This file contains the implementation of the Power class, which is
|
||||||
* of the device. It includes battery level sensing, power management unit (PMU) control, and power state machine management. The
|
* responsible for managing power-related functionality of the device. It
|
||||||
* Power class is used by the main device class to manage power-related functionality.
|
* includes battery level sensing, power management unit (PMU) control, and
|
||||||
|
* power state machine management. The Power class is used by the main device
|
||||||
|
* class to manage power-related functionality.
|
||||||
*
|
*
|
||||||
* The file also includes implementations of various battery level sensors, such as the AnalogBatteryLevel class, which assumes
|
* The file also includes implementations of various battery level sensors, such
|
||||||
* the battery voltage is attached via a voltage-divider to an analog input.
|
* as the AnalogBatteryLevel class, which assumes the battery voltage is
|
||||||
|
* attached via a voltage-divider to an analog input.
|
||||||
*
|
*
|
||||||
* This file is part of the Meshtastic project.
|
* This file is part of the Meshtastic project.
|
||||||
* For more information, see: https://meshtastic.org/
|
* For more information, see: https://meshtastic.org/
|
||||||
@@ -19,6 +22,7 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
|
#include "power/PowerHAL.h"
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
|
|
||||||
#if defined(ARCH_PORTDUINO)
|
#if defined(ARCH_PORTDUINO)
|
||||||
@@ -171,22 +175,12 @@ Power *power;
|
|||||||
|
|
||||||
using namespace meshtastic;
|
using namespace meshtastic;
|
||||||
|
|
||||||
#ifndef AREF_VOLTAGE
|
// NRF52 has AREF_VOLTAGE defined in architecture.h but
|
||||||
#if defined(ARCH_NRF52)
|
// make sure it's included. If something is wrong with NRF52
|
||||||
/*
|
// definition - compilation will fail on missing definition
|
||||||
* Internal Reference is +/-0.6V, with an adjustable gain of 1/6, 1/5, 1/4,
|
#if !defined(AREF_VOLTAGE) && !defined(ARCH_NRF52)
|
||||||
* 1/3, 1/2 or 1, meaning 3.6, 3.0, 2.4, 1.8, 1.2 or 0.6V for the ADC levels.
|
|
||||||
*
|
|
||||||
* External Reference is VDD/4, with an adjustable gain of 1, 2 or 4, meaning
|
|
||||||
* VDD/4, VDD/2 or VDD for the ADC levels.
|
|
||||||
*
|
|
||||||
* Default settings are internal reference with 1/6 gain (GND..3.6V ADC range)
|
|
||||||
*/
|
|
||||||
#define AREF_VOLTAGE 3.6
|
|
||||||
#else
|
|
||||||
#define AREF_VOLTAGE 3.3
|
#define AREF_VOLTAGE 3.3
|
||||||
#endif
|
#endif
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this board has a battery level sensor, set this to a valid implementation
|
* If this board has a battery level sensor, set this to a valid implementation
|
||||||
@@ -233,7 +227,8 @@ static void battery_adcDisable()
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple battery level sensor that assumes the battery voltage is attached via a voltage-divider to an analog input
|
* A simple battery level sensor that assumes the battery voltage is attached
|
||||||
|
* via a voltage-divider to an analog input
|
||||||
*/
|
*/
|
||||||
class AnalogBatteryLevel : public HasBatteryLevel
|
class AnalogBatteryLevel : public HasBatteryLevel
|
||||||
{
|
{
|
||||||
@@ -311,7 +306,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
|
|
||||||
#ifndef BATTERY_SENSE_SAMPLES
|
#ifndef BATTERY_SENSE_SAMPLES
|
||||||
#define BATTERY_SENSE_SAMPLES \
|
#define BATTERY_SENSE_SAMPLES \
|
||||||
15 // Set the number of samples, it has an effect of increasing sensitivity in complex electromagnetic environment.
|
15 // Set the number of samples, it has an effect of increasing sensitivity in
|
||||||
|
// complex electromagnetic environment.
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef BATTERY_PIN
|
#ifdef BATTERY_PIN
|
||||||
@@ -341,7 +337,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
battery_adcDisable();
|
battery_adcDisable();
|
||||||
|
|
||||||
if (!initial_read_done) {
|
if (!initial_read_done) {
|
||||||
// Flush the smoothing filter with an ADC reading, if the reading is plausibly correct
|
// Flush the smoothing filter with an ADC reading, if the reading is
|
||||||
|
// plausibly correct
|
||||||
if (scaled > last_read_value)
|
if (scaled > last_read_value)
|
||||||
last_read_value = scaled;
|
last_read_value = scaled;
|
||||||
initial_read_done = true;
|
initial_read_done = true;
|
||||||
@@ -350,8 +347,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF
|
last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF
|
||||||
}
|
}
|
||||||
|
|
||||||
// LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u", BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t)
|
// LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u",
|
||||||
// (last_read_value));
|
// BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t) (last_read_value));
|
||||||
}
|
}
|
||||||
return last_read_value;
|
return last_read_value;
|
||||||
#endif // BATTERY_PIN
|
#endif // BATTERY_PIN
|
||||||
@@ -420,7 +417,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
/**
|
/**
|
||||||
* return true if there is a battery installed in this unit
|
* return true if there is a battery installed in this unit
|
||||||
*/
|
*/
|
||||||
// if we have a integrated device with a battery, we can assume that the battery is always connected
|
// if we have a integrated device with a battery, we can assume that the
|
||||||
|
// battery is always connected
|
||||||
#ifdef BATTERY_IMMUTABLE
|
#ifdef BATTERY_IMMUTABLE
|
||||||
virtual bool isBatteryConnect() override { return true; }
|
virtual bool isBatteryConnect() override { return true; }
|
||||||
#elif defined(ADC_V)
|
#elif defined(ADC_V)
|
||||||
@@ -441,10 +439,10 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
|
virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// If we see a battery voltage higher than physics allows - assume charger is pumping
|
/// If we see a battery voltage higher than physics allows - assume charger is
|
||||||
/// in power
|
/// pumping in power On some boards we don't have the power management chip
|
||||||
/// On some boards we don't have the power management chip (like AXPxxxx)
|
/// (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to detect external power
|
||||||
/// so we use EXT_PWR_DETECT GPIO pin to detect external power source
|
/// source
|
||||||
virtual bool isVbusIn() override
|
virtual bool isVbusIn() override
|
||||||
{
|
{
|
||||||
#ifdef EXT_PWR_DETECT
|
#ifdef EXT_PWR_DETECT
|
||||||
@@ -461,8 +459,12 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
}
|
}
|
||||||
// if it's not HIGH - check the battery
|
// if it's not HIGH - check the battery
|
||||||
#endif
|
#endif
|
||||||
#elif defined(MUZI_BASE)
|
|
||||||
return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk;
|
// technically speaking this should work for all(?) NRF52 boards
|
||||||
|
// but needs testing across multiple devices. NRF52 USB would not even work if
|
||||||
|
// VBUS was not properly connected and detected by the CPU
|
||||||
|
#elif defined(MUZI_BASE) || defined(PROMICRO_DIY_TCXO)
|
||||||
|
return powerHAL_isVBUSConnected();
|
||||||
#endif
|
#endif
|
||||||
return getBattVoltage() > chargingVolt;
|
return getBattVoltage() > chargingVolt;
|
||||||
}
|
}
|
||||||
@@ -485,8 +487,9 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
#else
|
#else
|
||||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION)
|
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION)
|
||||||
if (hasINA()) {
|
if (hasINA()) {
|
||||||
// get current flow from INA sensor - negative value means power flowing into the battery
|
// get current flow from INA sensor - negative value means power flowing
|
||||||
// default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT RESISTOR <--> INA_VIN- <--> LOAD
|
// into the battery default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT
|
||||||
|
// RESISTOR <--> INA_VIN- <--> LOAD
|
||||||
LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address);
|
LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address);
|
||||||
#if defined(INA_CHARGING_DETECTION_INVERT)
|
#if defined(INA_CHARGING_DETECTION_INVERT)
|
||||||
return getINACurrent() > 0;
|
return getINACurrent() > 0;
|
||||||
@@ -502,8 +505,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// If we see a battery voltage higher than physics allows - assume charger is pumping
|
/// If we see a battery voltage higher than physics allows - assume charger is
|
||||||
/// in power
|
/// pumping in power
|
||||||
|
|
||||||
/// For heltecs with no battery connected, the measured voltage is 2204, so
|
/// For heltecs with no battery connected, the measured voltage is 2204, so
|
||||||
// need to be higher than that, in this case is 2500mV (3000-500)
|
// need to be higher than that, in this case is 2500mV (3000-500)
|
||||||
@@ -512,7 +515,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS;
|
const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS;
|
||||||
// Start value from minimum voltage for the filter to not start from 0
|
// Start value from minimum voltage for the filter to not start from 0
|
||||||
// that could trigger some events.
|
// that could trigger some events.
|
||||||
// This value is over-written by the first ADC reading, it the voltage seems reasonable.
|
// This value is over-written by the first ADC reading, it the voltage seems
|
||||||
|
// reasonable.
|
||||||
bool initial_read_done = false;
|
bool initial_read_done = false;
|
||||||
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
|
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
|
||||||
uint32_t last_read_time_ms = 0;
|
uint32_t last_read_time_ms = 0;
|
||||||
@@ -654,7 +658,8 @@ bool Power::analogInit()
|
|||||||
#ifdef CONFIG_IDF_TARGET_ESP32S3
|
#ifdef CONFIG_IDF_TARGET_ESP32S3
|
||||||
// ESP32S3
|
// ESP32S3
|
||||||
else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) {
|
else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) {
|
||||||
LOG_INFO("ADC config based on Two Point values and fitting curve coefficients stored in eFuse");
|
LOG_INFO("ADC config based on Two Point values and fitting curve "
|
||||||
|
"coefficients stored in eFuse");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
else {
|
else {
|
||||||
@@ -662,13 +667,7 @@ bool Power::analogInit()
|
|||||||
}
|
}
|
||||||
#endif // ARCH_ESP32
|
#endif // ARCH_ESP32
|
||||||
|
|
||||||
#ifdef ARCH_NRF52
|
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
|
||||||
#ifdef VBAT_AR_INTERNAL
|
|
||||||
analogReference(VBAT_AR_INTERNAL);
|
|
||||||
#else
|
|
||||||
analogReference(AR_INTERNAL); // 3.6V
|
|
||||||
#endif
|
|
||||||
#endif // ARCH_NRF52
|
|
||||||
|
|
||||||
#ifndef ARCH_ESP32
|
#ifndef ARCH_ESP32
|
||||||
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||||||
@@ -723,6 +722,16 @@ bool Power::setup()
|
|||||||
runASAP = true;
|
runASAP = true;
|
||||||
},
|
},
|
||||||
CHANGE);
|
CHANGE);
|
||||||
|
#endif
|
||||||
|
#ifdef EXT_CHRG_DETECT
|
||||||
|
attachInterrupt(
|
||||||
|
EXT_CHRG_DETECT,
|
||||||
|
[]() {
|
||||||
|
power->setIntervalFromNow(0);
|
||||||
|
runASAP = true;
|
||||||
|
BaseType_t higherWake = 0;
|
||||||
|
},
|
||||||
|
CHANGE);
|
||||||
#endif
|
#endif
|
||||||
enabled = found;
|
enabled = found;
|
||||||
low_voltage_counter = 0;
|
low_voltage_counter = 0;
|
||||||
@@ -769,7 +778,8 @@ void Power::reboot()
|
|||||||
HAL_NVIC_SystemReset();
|
HAL_NVIC_SystemReset();
|
||||||
#else
|
#else
|
||||||
rebootAtMsec = -1;
|
rebootAtMsec = -1;
|
||||||
LOG_WARN("FIXME implement reboot for this platform. Note that some settings require a restart to be applied");
|
LOG_WARN("FIXME implement reboot for this platform. Note that some settings "
|
||||||
|
"require a restart to be applied");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -779,9 +789,12 @@ void Power::shutdown()
|
|||||||
#if HAS_SCREEN
|
#if HAS_SCREEN
|
||||||
if (screen) {
|
if (screen) {
|
||||||
#ifdef T_DECK_PRO
|
#ifdef T_DECK_PRO
|
||||||
screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button
|
screen->showSimpleBanner("Device is powered off.\nConnect USB to start!",
|
||||||
|
0); // T-Deck Pro has no power button
|
||||||
#elif defined(USE_EINK)
|
#elif defined(USE_EINK)
|
||||||
screen->showSimpleBanner("Shutting Down...", 2250); // dismiss after 3 seconds to avoid the banner on the sleep screen
|
screen->showSimpleBanner("Shutting Down...",
|
||||||
|
2250); // dismiss after 3 seconds to avoid the
|
||||||
|
// banner on the sleep screen
|
||||||
#else
|
#else
|
||||||
screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
|
screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
|
||||||
#endif
|
#endif
|
||||||
@@ -803,6 +816,9 @@ void Power::shutdown()
|
|||||||
#endif
|
#endif
|
||||||
#ifdef PIN_LED3
|
#ifdef PIN_LED3
|
||||||
ledOff(PIN_LED3);
|
ledOff(PIN_LED3);
|
||||||
|
#endif
|
||||||
|
#ifdef LED_NOTIFICATION
|
||||||
|
ledOff(LED_NOTIFICATION);
|
||||||
#endif
|
#endif
|
||||||
doDeepSleep(DELAY_FOREVER, true, true);
|
doDeepSleep(DELAY_FOREVER, true, true);
|
||||||
#elif defined(ARCH_PORTDUINO)
|
#elif defined(ARCH_PORTDUINO)
|
||||||
@@ -820,7 +836,8 @@ void Power::readPowerStatus()
|
|||||||
int32_t batteryVoltageMv = -1; // Assume unknown
|
int32_t batteryVoltageMv = -1; // Assume unknown
|
||||||
int8_t batteryChargePercent = -1;
|
int8_t batteryChargePercent = -1;
|
||||||
OptionalBool usbPowered = OptUnknown;
|
OptionalBool usbPowered = OptUnknown;
|
||||||
OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM code doesn't run every time
|
OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM
|
||||||
|
// code doesn't run every time
|
||||||
OptionalBool isChargingNow = OptUnknown;
|
OptionalBool isChargingNow = OptUnknown;
|
||||||
|
|
||||||
if (batteryLevel) {
|
if (batteryLevel) {
|
||||||
@@ -833,9 +850,10 @@ void Power::readPowerStatus()
|
|||||||
if (batteryLevel->getBatteryPercent() >= 0) {
|
if (batteryLevel->getBatteryPercent() >= 0) {
|
||||||
batteryChargePercent = batteryLevel->getBatteryPercent();
|
batteryChargePercent = batteryLevel->getBatteryPercent();
|
||||||
} else {
|
} else {
|
||||||
// If the AXP192 returns a percentage less than 0, the feature is either not supported or there is an error
|
// If the AXP192 returns a percentage less than 0, the feature is either
|
||||||
// In that case, we compute an estimate of the charge percent based on open circuit voltage table defined
|
// not supported or there is an error In that case, we compute an
|
||||||
// in power.h
|
// estimate of the charge percent based on open circuit voltage table
|
||||||
|
// defined in power.h
|
||||||
batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) /
|
batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) /
|
||||||
((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))),
|
((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))),
|
||||||
0, 100);
|
0, 100);
|
||||||
@@ -843,12 +861,12 @@ void Power::readPowerStatus()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way better instead to make a Nrf52IsUsbPowered subclass
|
// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way
|
||||||
// (which shares a superclass with the BatteryLevel stuff)
|
// better instead to make a Nrf52IsUsbPowered subclass (which shares a
|
||||||
// that just provides a few methods. But in the interest of fixing this bug I'm going to follow current
|
// superclass with the BatteryLevel stuff) that just provides a few methods. But
|
||||||
// practice.
|
// in the interest of fixing this bug I'm going to follow current practice.
|
||||||
#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates the power states. Takes 20 seconds or so to detect
|
#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates
|
||||||
// changes.
|
// the power states. Takes 20 seconds or so to detect changes.
|
||||||
|
|
||||||
nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get();
|
nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get();
|
||||||
// LOG_DEBUG("NRF Power %d", nrf_usb_state);
|
// LOG_DEBUG("NRF Power %d", nrf_usb_state);
|
||||||
@@ -922,8 +940,9 @@ void Power::readPowerStatus()
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// If we have a battery at all and it is less than 0%, force deep sleep if we have more than 10 low readings in
|
// If we have a battery at all and it is less than 0%, force deep sleep if we
|
||||||
// a row. NOTE: min LiIon/LiPo voltage is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough.
|
// have more than 10 low readings in a row. NOTE: min LiIon/LiPo voltage
|
||||||
|
// is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough.
|
||||||
//
|
//
|
||||||
|
|
||||||
if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) {
|
if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) {
|
||||||
@@ -945,8 +964,8 @@ int32_t Power::runOnce()
|
|||||||
readPowerStatus();
|
readPowerStatus();
|
||||||
|
|
||||||
#ifdef HAS_PMU
|
#ifdef HAS_PMU
|
||||||
// WE no longer use the IRQ line to wake the CPU (due to false wakes from sleep), but we do poll
|
// WE no longer use the IRQ line to wake the CPU (due to false wakes from
|
||||||
// the IRQ status by reading the registers over I2C
|
// sleep), but we do poll the IRQ status by reading the registers over I2C
|
||||||
if (PMU) {
|
if (PMU) {
|
||||||
|
|
||||||
PMU->getIrqStatus();
|
PMU->getIrqStatus();
|
||||||
@@ -988,7 +1007,8 @@ int32_t Power::runOnce()
|
|||||||
PMU->clearIrqStatus();
|
PMU->clearIrqStatus();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
// Only read once every 20 seconds once the power status for the app has been initialized
|
// Only read once every 20 seconds once the power status for the app has been
|
||||||
|
// initialized
|
||||||
return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME;
|
return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -996,10 +1016,12 @@ int32_t Power::runOnce()
|
|||||||
* Init the power manager chip
|
* Init the power manager chip
|
||||||
*
|
*
|
||||||
* axp192 power
|
* axp192 power
|
||||||
DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose comms to the axp192 because the OLED and the
|
DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose
|
||||||
axp192 share the same i2c bus, instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> ESP32 (keep this
|
comms to the axp192 because the OLED and the axp192 share the same i2c bus,
|
||||||
on!) LDO1 30mA -> charges GPS backup battery // charges the tiny J13 battery by the GPS to power the GPS ram (for a couple of
|
instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max ->
|
||||||
days), can not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS
|
ESP32 (keep this on!) LDO1 30mA -> charges GPS backup battery // charges the
|
||||||
|
tiny J13 battery by the GPS to power the GPS ram (for a couple of days), can
|
||||||
|
not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
bool Power::axpChipInit()
|
bool Power::axpChipInit()
|
||||||
@@ -1044,9 +1066,10 @@ bool Power::axpChipInit()
|
|||||||
|
|
||||||
if (!PMU) {
|
if (!PMU) {
|
||||||
/*
|
/*
|
||||||
* In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will be called at the same time.
|
* In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will
|
||||||
* In order not to affect other devices, if the initialization of the PMU fails, Wire needs to be re-initialized once,
|
* be called at the same time. In order not to affect other devices, if the
|
||||||
* if there are multiple devices sharing the bus.
|
* initialization of the PMU fails, Wire needs to be re-initialized once, if
|
||||||
|
* there are multiple devices sharing the bus.
|
||||||
* * */
|
* * */
|
||||||
#ifndef PMU_USE_WIRE1
|
#ifndef PMU_USE_WIRE1
|
||||||
w->begin(I2C_SDA, I2C_SCL);
|
w->begin(I2C_SDA, I2C_SCL);
|
||||||
@@ -1063,8 +1086,8 @@ bool Power::axpChipInit()
|
|||||||
PMU->enablePowerOutput(XPOWERS_LDO2);
|
PMU->enablePowerOutput(XPOWERS_LDO2);
|
||||||
|
|
||||||
// oled module power channel,
|
// oled module power channel,
|
||||||
// disable it will cause abnormal communication between boot and AXP power supply,
|
// disable it will cause abnormal communication between boot and AXP power
|
||||||
// do not turn it off
|
// supply, do not turn it off
|
||||||
PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300);
|
PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300);
|
||||||
// enable oled power
|
// enable oled power
|
||||||
PMU->enablePowerOutput(XPOWERS_DCDC1);
|
PMU->enablePowerOutput(XPOWERS_DCDC1);
|
||||||
@@ -1091,7 +1114,8 @@ bool Power::axpChipInit()
|
|||||||
PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2);
|
PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2);
|
||||||
} else if (PMU->getChipModel() == XPOWERS_AXP2101) {
|
} else if (PMU->getChipModel() == XPOWERS_AXP2101) {
|
||||||
|
|
||||||
/*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it uses an AXP2101 power chip*/
|
/*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it
|
||||||
|
* uses an AXP2101 power chip*/
|
||||||
if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) {
|
if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) {
|
||||||
// Unuse power channel
|
// Unuse power channel
|
||||||
PMU->disablePowerOutput(XPOWERS_DCDC2);
|
PMU->disablePowerOutput(XPOWERS_DCDC2);
|
||||||
@@ -1126,8 +1150,8 @@ bool Power::axpChipInit()
|
|||||||
// t-beam s3 core
|
// t-beam s3 core
|
||||||
/**
|
/**
|
||||||
* gnss module power channel
|
* gnss module power channel
|
||||||
* The default ALDO4 is off, you need to turn on the GNSS power first, otherwise it will be invalid during
|
* The default ALDO4 is off, you need to turn on the GNSS power first,
|
||||||
* initialization
|
* otherwise it will be invalid during initialization
|
||||||
*/
|
*/
|
||||||
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
|
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
|
||||||
PMU->enablePowerOutput(XPOWERS_ALDO4);
|
PMU->enablePowerOutput(XPOWERS_ALDO4);
|
||||||
@@ -1177,7 +1201,8 @@ bool Power::axpChipInit()
|
|||||||
// disable all axp chip interrupt
|
// disable all axp chip interrupt
|
||||||
PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
|
PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
|
||||||
|
|
||||||
// Set the constant current charging current of AXP2101, temporarily use 500mA by default
|
// Set the constant current charging current of AXP2101, temporarily use
|
||||||
|
// 500mA by default
|
||||||
PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA);
|
PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA);
|
||||||
|
|
||||||
// Set up the charging voltage
|
// Set up the charging voltage
|
||||||
@@ -1243,11 +1268,12 @@ bool Power::axpChipInit()
|
|||||||
PMU->getPowerChannelVoltage(XPOWERS_BLDO2));
|
PMU->getPowerChannelVoltage(XPOWERS_BLDO2));
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can safely ignore this approach for most (or all) boards because MCU turned off
|
// We can safely ignore this approach for most (or all) boards because MCU
|
||||||
// earlier than battery discharged to 2.6V.
|
// turned off earlier than battery discharged to 2.6V.
|
||||||
//
|
//
|
||||||
// Unfortanly for now we can't use this killswitch for RAK4630-based boards because they have a bug with
|
// Unfortunately for now we can't use this killswitch for RAK4630-based boards
|
||||||
// battery voltage measurement. Probably it sometimes drops to low values.
|
// because they have a bug with battery voltage measurement. Probably it
|
||||||
|
// sometimes drops to low values.
|
||||||
#ifndef RAK4630
|
#ifndef RAK4630
|
||||||
// Set PMU shutdown voltage at 2.6V to maximize battery utilization
|
// Set PMU shutdown voltage at 2.6V to maximize battery utilization
|
||||||
PMU->setSysPowerDownVoltage(2600);
|
PMU->setSysPowerDownVoltage(2600);
|
||||||
@@ -1266,10 +1292,12 @@ bool Power::axpChipInit()
|
|||||||
attachInterrupt(
|
attachInterrupt(
|
||||||
PMU_IRQ, [] { pmu_irq = true; }, FALLING);
|
PMU_IRQ, [] { pmu_irq = true; }, FALLING);
|
||||||
|
|
||||||
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ because it occurs repeatedly while there is
|
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ
|
||||||
// no battery also it could cause inadvertent waking from light sleep just because the battery filled
|
// because it occurs repeatedly while there is no battery also it could cause
|
||||||
// we don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while no battery installed
|
// inadvertent waking from light sleep just because the battery filled we
|
||||||
// we don't look at AXPXXX_VBUS_REMOVED_IRQ because we don't have anything hooked to vbus
|
// don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while
|
||||||
|
// no battery installed we don't look at AXPXXX_VBUS_REMOVED_IRQ because we
|
||||||
|
// don't have anything hooked to vbus
|
||||||
PMU->enableIRQ(pmuIrqMask);
|
PMU->enableIRQ(pmuIrqMask);
|
||||||
|
|
||||||
PMU->clearIrqStatus();
|
PMU->clearIrqStatus();
|
||||||
@@ -1385,8 +1413,8 @@ class LipoCharger : public HasBatteryLevel
|
|||||||
bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
|
bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
|
||||||
if (result) {
|
if (result) {
|
||||||
LOG_INFO("PPM BQ25896 init succeeded");
|
LOG_INFO("PPM BQ25896 init succeeded");
|
||||||
// Set the minimum operating voltage. Below this voltage, the PPM will protect
|
// Set the minimum operating voltage. Below this voltage, the PPM will
|
||||||
// PPM->setSysPowerDownVoltage(3100);
|
// protect PPM->setSysPowerDownVoltage(3100);
|
||||||
|
|
||||||
// Set input current limit, default is 500mA
|
// Set input current limit, default is 500mA
|
||||||
// PPM->setInputCurrentLimit(800);
|
// PPM->setInputCurrentLimit(800);
|
||||||
@@ -1409,7 +1437,8 @@ class LipoCharger : public HasBatteryLevel
|
|||||||
PPM->enableMeasure();
|
PPM->enableMeasure();
|
||||||
|
|
||||||
// Turn on charging function
|
// Turn on charging function
|
||||||
// If there is no battery connected, do not turn on the charging function
|
// If there is no battery connected, do not turn on the charging
|
||||||
|
// function
|
||||||
PPM->enableCharge();
|
PPM->enableCharge();
|
||||||
} else {
|
} else {
|
||||||
LOG_WARN("PPM BQ25896 init failed");
|
LOG_WARN("PPM BQ25896 init failed");
|
||||||
@@ -1444,7 +1473,8 @@ class LipoCharger : public HasBatteryLevel
|
|||||||
virtual int getBatteryPercent() override
|
virtual int getBatteryPercent() override
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
// return bq->getChargePercent(); // don't use BQ27220 for battery percent, it is not calibrated
|
// return bq->getChargePercent(); // don't use BQ27220 for battery percent,
|
||||||
|
// it is not calibrated
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1566,7 +1596,8 @@ bool Power::meshSolarInit()
|
|||||||
|
|
||||||
#else
|
#else
|
||||||
/**
|
/**
|
||||||
* The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel
|
* The meshSolar battery level sensor is unavailable - default to
|
||||||
|
* AnalogBatteryLevel
|
||||||
*/
|
*/
|
||||||
bool Power::meshSolarInit()
|
bool Power::meshSolarInit()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -155,6 +155,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Default system gain to 0 if not defined
|
// Default system gain to 0 if not defined
|
||||||
|
#ifndef NUM_PA_POINTS
|
||||||
|
#define NUM_PA_POINTS 1
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifndef TX_GAIN_LORA
|
#ifndef TX_GAIN_LORA
|
||||||
#define TX_GAIN_LORA 0
|
#define TX_GAIN_LORA 0
|
||||||
#endif
|
#endif
|
||||||
@@ -386,9 +390,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#ifndef HAS_RADIO
|
#ifndef HAS_RADIO
|
||||||
#define HAS_RADIO 0
|
#define HAS_RADIO 0
|
||||||
#endif
|
#endif
|
||||||
#ifndef HAS_RTC
|
|
||||||
#define HAS_RTC 0
|
|
||||||
#endif
|
|
||||||
#ifndef HAS_CPU_SHUTDOWN
|
#ifndef HAS_CPU_SHUTDOWN
|
||||||
#define HAS_CPU_SHUTDOWN 0
|
#define HAS_CPU_SHUTDOWN 0
|
||||||
#endif
|
#endif
|
||||||
@@ -424,12 +425,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define HAS_RGB_LED
|
#define HAS_RGB_LED
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef LED_STATE_OFF
|
|
||||||
#define LED_STATE_OFF 0
|
|
||||||
#endif
|
|
||||||
#ifndef LED_STATE_ON
|
#ifndef LED_STATE_ON
|
||||||
#define LED_STATE_ON 1
|
#define LED_STATE_ON 1
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef LED_STATE_OFF
|
||||||
|
#define LED_STATE_OFF (LED_STATE_ON ^ 1)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef ledOff
|
||||||
|
#define ledOff(pin) pinMode(pin, INPUT)
|
||||||
|
#endif
|
||||||
|
|
||||||
// default mapping of pins
|
// default mapping of pins
|
||||||
#if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN)
|
#if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN)
|
||||||
@@ -445,18 +450,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// BME680 BSEC2 support detection
|
|
||||||
#if !defined(MESHTASTIC_BME680_BSEC2_SUPPORTED)
|
|
||||||
#if defined(RAK_4631) || defined(TBEAM_V10)
|
|
||||||
|
|
||||||
#define MESHTASTIC_BME680_BSEC2_SUPPORTED 1
|
|
||||||
#define MESHTASTIC_BME680_HEADER <bsec2.h>
|
|
||||||
#else
|
|
||||||
#define MESHTASTIC_BME680_BSEC2_SUPPORTED 0
|
|
||||||
#define MESHTASTIC_BME680_HEADER <Adafruit_BME680.h>
|
|
||||||
#endif // defined(RAK_4631)
|
|
||||||
#endif // !defined(MESHTASTIC_BME680_BSEC2_SUPPORTED)
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Global switches to turn off features for a minimized build
|
// Global switches to turn off features for a minimized build
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -905,6 +905,12 @@ void GPS::writePinStandby(bool standby)
|
|||||||
// Write and log
|
// Write and log
|
||||||
pinMode(PIN_GPS_STANDBY, OUTPUT);
|
pinMode(PIN_GPS_STANDBY, OUTPUT);
|
||||||
digitalWrite(PIN_GPS_STANDBY, val);
|
digitalWrite(PIN_GPS_STANDBY, val);
|
||||||
|
|
||||||
|
// Enter backup mode on PA1010D; TODO: may be applicable to other MTK GPS too
|
||||||
|
if (IS_ONE_OF(gnssModel, GNSS_MODEL_MTK_PA1010D)) {
|
||||||
|
_serial_gps->write("$PMTK225,4*2F\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef GPS_DEBUG
|
#ifdef GPS_DEBUG
|
||||||
LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW");
|
LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW");
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -72,11 +72,13 @@ RTCSetResult readFromRTC()
|
|||||||
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
|
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
|
||||||
#if defined(PCF8563_RTC)
|
#if defined(PCF8563_RTC)
|
||||||
if (rtc_found.address == PCF8563_RTC) {
|
if (rtc_found.address == PCF8563_RTC) {
|
||||||
|
SensorPCF8563 rtc;
|
||||||
#elif defined(PCF85063_RTC)
|
#elif defined(PCF85063_RTC)
|
||||||
if (rtc_found.address == PCF85063_RTC) {
|
if (rtc_found.address == PCF85063_RTC) {
|
||||||
|
SensorPCF85063 rtc;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
SensorRtcHelper rtc;
|
|
||||||
|
|
||||||
#if WIRE_INTERFACES_COUNT == 2
|
#if WIRE_INTERFACES_COUNT == 2
|
||||||
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
|
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
|
||||||
@@ -240,10 +242,12 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
|||||||
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
|
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
|
||||||
#if defined(PCF8563_RTC)
|
#if defined(PCF8563_RTC)
|
||||||
if (rtc_found.address == PCF8563_RTC) {
|
if (rtc_found.address == PCF8563_RTC) {
|
||||||
|
SensorPCF8563 rtc;
|
||||||
#elif defined(PCF85063_RTC)
|
#elif defined(PCF85063_RTC)
|
||||||
if (rtc_found.address == PCF85063_RTC) {
|
if (rtc_found.address == PCF85063_RTC) {
|
||||||
|
SensorPCF85063 rtc;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
SensorRtcHelper rtc;
|
|
||||||
|
|
||||||
#if WIRE_INTERFACES_COUNT == 2
|
#if WIRE_INTERFACES_COUNT == 2
|
||||||
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
|
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
|
||||||
@@ -276,11 +280,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
|||||||
settimeofday(tv, NULL);
|
settimeofday(tv, NULL);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// nrf52 doesn't have a readable RTC (yet - software not written)
|
|
||||||
#if HAS_RTC
|
|
||||||
readFromRTC();
|
readFromRTC();
|
||||||
#endif
|
|
||||||
|
|
||||||
return RTCSetResultSuccess;
|
return RTCSetResultSuccess;
|
||||||
} else {
|
} else {
|
||||||
return RTCSetResultNotSet; // RTC was already set with a higher quality time
|
return RTCSetResultNotSet; // RTC was already set with a higher quality time
|
||||||
@@ -397,7 +397,7 @@ uint32_t getValidTime(RTCQuality minQuality, bool local)
|
|||||||
return (currentQuality >= minQuality) ? getTime(local) : 0;
|
return (currentQuality >= minQuality) ? getTime(local) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
time_t gm_mktime(struct tm *tm)
|
time_t gm_mktime(const struct tm *tm)
|
||||||
{
|
{
|
||||||
#if !MESHTASTIC_EXCLUDE_TZ
|
#if !MESHTASTIC_EXCLUDE_TZ
|
||||||
time_t result = 0;
|
time_t result = 0;
|
||||||
@@ -413,8 +413,8 @@ time_t gm_mktime(struct tm *tm)
|
|||||||
days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400);
|
days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400);
|
||||||
|
|
||||||
// Now, within this tm->year, compute the days *before* this tm->month starts.
|
// Now, within this tm->year, compute the days *before* this tm->month starts.
|
||||||
int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year
|
static const int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year
|
||||||
int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11
|
int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11
|
||||||
|
|
||||||
// If this is a leap year, and we're past February, add a day:
|
// If this is a leap year, and we're past February, add a day:
|
||||||
if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) {
|
if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) {
|
||||||
@@ -435,6 +435,7 @@ time_t gm_mktime(struct tm *tm)
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
#else
|
#else
|
||||||
return mktime(tm);
|
struct tm tmCopy = *tm;
|
||||||
|
return mktime(&tmCopy);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ uint32_t getValidTime(RTCQuality minQuality, bool local = false);
|
|||||||
|
|
||||||
RTCSetResult readFromRTC();
|
RTCSetResult readFromRTC();
|
||||||
|
|
||||||
time_t gm_mktime(struct tm *tm);
|
time_t gm_mktime(const struct tm *tm);
|
||||||
|
|
||||||
#define SEC_PER_DAY 86400
|
#define SEC_PER_DAY 86400
|
||||||
#define SEC_PER_HOUR 3600
|
#define SEC_PER_HOUR 3600
|
||||||
|
|||||||
@@ -1731,6 +1731,26 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
showFrame(FrameDirection::PREVIOUS);
|
showFrame(FrameDirection::PREVIOUS);
|
||||||
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
|
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||||
showFrame(FrameDirection::NEXT);
|
showFrame(FrameDirection::NEXT);
|
||||||
|
} else if (event->inputEvent == INPUT_BROKER_FN_F1) {
|
||||||
|
this->ui->switchToFrame(0);
|
||||||
|
lastScreenTransition = millis();
|
||||||
|
setFastFramerate();
|
||||||
|
} else if (event->inputEvent == INPUT_BROKER_FN_F2) {
|
||||||
|
this->ui->switchToFrame(1);
|
||||||
|
lastScreenTransition = millis();
|
||||||
|
setFastFramerate();
|
||||||
|
} else if (event->inputEvent == INPUT_BROKER_FN_F3) {
|
||||||
|
this->ui->switchToFrame(2);
|
||||||
|
lastScreenTransition = millis();
|
||||||
|
setFastFramerate();
|
||||||
|
} else if (event->inputEvent == INPUT_BROKER_FN_F4) {
|
||||||
|
this->ui->switchToFrame(3);
|
||||||
|
lastScreenTransition = millis();
|
||||||
|
setFastFramerate();
|
||||||
|
} else if (event->inputEvent == INPUT_BROKER_FN_F5) {
|
||||||
|
this->ui->switchToFrame(4);
|
||||||
|
lastScreenTransition = millis();
|
||||||
|
setFastFramerate();
|
||||||
} else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
} else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
||||||
// Long press up button for fast frame switching
|
// Long press up button for fast frame switching
|
||||||
showPrevFrame();
|
showPrevFrame();
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
if (currentResolution == ScreenResolution::UltraLow) {
|
if (currentResolution == ScreenResolution::UltraLow) {
|
||||||
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num);
|
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num);
|
||||||
} else {
|
} else {
|
||||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num);
|
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz (%d)", freqStr, config.lora.channel_num);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
size_t len = strlen(frequencyslot);
|
size_t len = strlen(frequencyslot);
|
||||||
|
|||||||
@@ -65,12 +65,12 @@ uint8_t test_count = 0;
|
|||||||
|
|
||||||
void menuHandler::loraMenu()
|
void menuHandler::loraMenu()
|
||||||
{
|
{
|
||||||
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "LoRa Region"};
|
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"};
|
||||||
enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, lora_picker = 3 };
|
enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, frequency_slot = 3, lora_picker = 4 };
|
||||||
BannerOverlayOptions bannerOptions;
|
BannerOverlayOptions bannerOptions;
|
||||||
bannerOptions.message = "LoRa Actions";
|
bannerOptions.message = "LoRa Actions";
|
||||||
bannerOptions.optionsArrayPtr = optionsArray;
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
bannerOptions.optionsCount = 4;
|
bannerOptions.optionsCount = 5;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == Back) {
|
if (selected == Back) {
|
||||||
// No action
|
// No action
|
||||||
@@ -78,6 +78,8 @@ void menuHandler::loraMenu()
|
|||||||
menuHandler::menuQueue = menuHandler::device_role_picker;
|
menuHandler::menuQueue = menuHandler::device_role_picker;
|
||||||
} else if (selected == radio_preset_picker) {
|
} else if (selected == radio_preset_picker) {
|
||||||
menuHandler::menuQueue = menuHandler::radio_preset_picker;
|
menuHandler::menuQueue = menuHandler::radio_preset_picker;
|
||||||
|
} else if (selected == frequency_slot) {
|
||||||
|
menuHandler::menuQueue = menuHandler::frequency_slot;
|
||||||
} else if (selected == lora_picker) {
|
} else if (selected == lora_picker) {
|
||||||
menuHandler::menuQueue = menuHandler::lora_picker;
|
menuHandler::menuQueue = menuHandler::lora_picker;
|
||||||
}
|
}
|
||||||
@@ -248,6 +250,69 @@ void menuHandler::DeviceRolePicker()
|
|||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void menuHandler::FrequencySlotPicker()
|
||||||
|
{
|
||||||
|
|
||||||
|
enum ReplyOptions : int { Back = -1 };
|
||||||
|
constexpr int MAX_CHANNEL_OPTIONS = 202;
|
||||||
|
static const char *optionsArray[MAX_CHANNEL_OPTIONS];
|
||||||
|
static int optionsEnumArray[MAX_CHANNEL_OPTIONS];
|
||||||
|
static char channelText[MAX_CHANNEL_OPTIONS - 1][12];
|
||||||
|
int options = 0;
|
||||||
|
optionsArray[options] = "Back";
|
||||||
|
optionsEnumArray[options++] = Back;
|
||||||
|
optionsArray[options] = "Slot 0 (Auto)";
|
||||||
|
optionsEnumArray[options++] = 0;
|
||||||
|
|
||||||
|
// Calculate number of channels (copied from RadioInterface::applyModemConfig())
|
||||||
|
meshtastic_Config_LoRaConfig &loraConfig = config.lora;
|
||||||
|
double bw = loraConfig.use_preset ? modemPresetToBwKHz(loraConfig.modem_preset, myRegion->wideLora)
|
||||||
|
: bwCodeToKHz(loraConfig.bandwidth);
|
||||||
|
|
||||||
|
uint32_t numChannels = 0;
|
||||||
|
if (myRegion) {
|
||||||
|
numChannels = (uint32_t)floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000.0)));
|
||||||
|
} else {
|
||||||
|
LOG_WARN("Region not set, cannot calculate number of channels");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numChannels > (uint32_t)(MAX_CHANNEL_OPTIONS - 2))
|
||||||
|
numChannels = (uint32_t)(MAX_CHANNEL_OPTIONS - 2);
|
||||||
|
|
||||||
|
for (uint32_t ch = 1; ch <= numChannels; ch++) {
|
||||||
|
snprintf(channelText[ch - 1], sizeof(channelText[ch - 1]), "Slot %lu", (unsigned long)ch);
|
||||||
|
optionsArray[options] = channelText[ch - 1];
|
||||||
|
optionsEnumArray[options++] = (int)ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
BannerOverlayOptions bannerOptions;
|
||||||
|
bannerOptions.message = "Frequency Slot";
|
||||||
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
|
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||||
|
bannerOptions.optionsCount = options;
|
||||||
|
|
||||||
|
// Start highlight on current channel if possible, otherwise on "1"
|
||||||
|
int initial = (int)config.lora.channel_num + 1;
|
||||||
|
if (initial < 2 || initial > (int)numChannels + 1)
|
||||||
|
initial = 1;
|
||||||
|
bannerOptions.InitialSelected = initial;
|
||||||
|
|
||||||
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
|
if (selected == Back) {
|
||||||
|
menuHandler::menuQueue = menuHandler::lora_Menu;
|
||||||
|
screen->runNow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.lora.channel_num = selected;
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
screen->showOverlayBanner(bannerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
void menuHandler::RadioPresetPicker()
|
void menuHandler::RadioPresetPicker()
|
||||||
{
|
{
|
||||||
static const RadioPresetOption presetOptions[] = {
|
static const RadioPresetOption presetOptions[] = {
|
||||||
@@ -278,6 +343,8 @@ void menuHandler::RadioPresetPicker()
|
|||||||
}
|
}
|
||||||
|
|
||||||
config.lora.modem_preset = option.value;
|
config.lora.modem_preset = option.value;
|
||||||
|
config.lora.channel_num = 0; // Reset to default channel for the preset
|
||||||
|
config.lora.override_frequency = 0; // Clear any custom frequency
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||||
});
|
});
|
||||||
@@ -2551,6 +2618,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
|||||||
case radio_preset_picker:
|
case radio_preset_picker:
|
||||||
RadioPresetPicker();
|
RadioPresetPicker();
|
||||||
break;
|
break;
|
||||||
|
case frequency_slot:
|
||||||
|
FrequencySlotPicker();
|
||||||
|
break;
|
||||||
case no_timeout_lora_picker:
|
case no_timeout_lora_picker:
|
||||||
LoraRegionPicker(0);
|
LoraRegionPicker(0);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class menuHandler
|
|||||||
lora_picker,
|
lora_picker,
|
||||||
device_role_picker,
|
device_role_picker,
|
||||||
radio_preset_picker,
|
radio_preset_picker,
|
||||||
|
frequency_slot,
|
||||||
no_timeout_lora_picker,
|
no_timeout_lora_picker,
|
||||||
TZ_picker,
|
TZ_picker,
|
||||||
twelve_hour_picker,
|
twelve_hour_picker,
|
||||||
@@ -63,6 +64,7 @@ class menuHandler
|
|||||||
static void loraMenu();
|
static void loraMenu();
|
||||||
static void DeviceRolePicker();
|
static void DeviceRolePicker();
|
||||||
static void RadioPresetPicker();
|
static void RadioPresetPicker();
|
||||||
|
static void FrequencySlotPicker();
|
||||||
static void handleMenuSwitch(OLEDDisplay *display);
|
static void handleMenuSwitch(OLEDDisplay *display);
|
||||||
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
|
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
|
||||||
static void clockMenu();
|
static void clockMenu();
|
||||||
@@ -138,7 +140,7 @@ struct ScreenColor {
|
|||||||
uint8_t b;
|
uint8_t b;
|
||||||
bool useVariant;
|
bool useVariant;
|
||||||
|
|
||||||
ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
|
explicit ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
|
||||||
: r(rIn), g(gIn), b(bIn), useVariant(variantIn)
|
: r(rIn), g(gIn), b(bIn), useVariant(variantIn)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
#include "MessageStore.h"
|
#include "MessageStore.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "UIRenderer.h"
|
#include "UIRenderer.h"
|
||||||
#include "configuration.h"
|
|
||||||
#include "gps/RTC.h"
|
#include "gps/RTC.h"
|
||||||
#include "graphics/Screen.h"
|
#include "graphics/Screen.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
@@ -20,7 +19,6 @@
|
|||||||
|
|
||||||
// External declarations
|
// External declarations
|
||||||
extern bool hasUnreadMessage;
|
extern bool hasUnreadMessage;
|
||||||
extern meshtastic_DeviceState devicestate;
|
|
||||||
extern graphics::Screen *screen;
|
extern graphics::Screen *screen;
|
||||||
|
|
||||||
using graphics::Emote;
|
using graphics::Emote;
|
||||||
@@ -49,7 +47,7 @@ static inline size_t utf8CharLen(uint8_t c)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove variation selectors (FE0F) and skin tone modifiers from emoji so they match your labels
|
// Remove variation selectors (FE0F) and skin tone modifiers from emoji so they match your labels
|
||||||
std::string normalizeEmoji(const std::string &s)
|
static std::string normalizeEmoji(const std::string &s)
|
||||||
{
|
{
|
||||||
std::string out;
|
std::string out;
|
||||||
for (size_t i = 0; i < s.size();) {
|
for (size_t i = 0; i < s.size();) {
|
||||||
@@ -82,6 +80,7 @@ uint32_t pauseStart = 0;
|
|||||||
bool waitingToReset = false;
|
bool waitingToReset = false;
|
||||||
bool scrollStarted = false;
|
bool scrollStarted = false;
|
||||||
static bool didReset = false;
|
static bool didReset = false;
|
||||||
|
static constexpr int MESSAGE_BLOCK_GAP = 6;
|
||||||
|
|
||||||
void scrollUp()
|
void scrollUp()
|
||||||
{
|
{
|
||||||
@@ -111,22 +110,6 @@ void scrollDown()
|
|||||||
|
|
||||||
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
|
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
|
||||||
{
|
{
|
||||||
std::string renderLine;
|
|
||||||
for (size_t i = 0; i < line.size();) {
|
|
||||||
uint8_t c = (uint8_t)line[i];
|
|
||||||
size_t len = utf8CharLen(c);
|
|
||||||
if (c == 0xEF && i + 2 < line.size() && (uint8_t)line[i + 1] == 0xB8 && (uint8_t)line[i + 2] == 0x8F) {
|
|
||||||
i += 3;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (c == 0xF0 && i + 3 < line.size() && (uint8_t)line[i + 1] == 0x9F && (uint8_t)line[i + 2] == 0x8F &&
|
|
||||||
((uint8_t)line[i + 3] >= 0xBB && (uint8_t)line[i + 3] <= 0xBF)) {
|
|
||||||
i += 4;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
renderLine.append(line, i, len);
|
|
||||||
i += len;
|
|
||||||
}
|
|
||||||
int cursorX = x;
|
int cursorX = x;
|
||||||
const int fontHeight = FONT_HEIGHT_SMALL;
|
const int fontHeight = FONT_HEIGHT_SMALL;
|
||||||
|
|
||||||
@@ -203,8 +186,7 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
|
|||||||
|
|
||||||
// Render the emote (if found)
|
// Render the emote (if found)
|
||||||
if (matchedEmote && i == nextEmotePos) {
|
if (matchedEmote && i == nextEmotePos) {
|
||||||
// Vertically center emote relative to font baseline (not just midline)
|
int iconY = y + (lineHeight - matchedEmote->height) / 2;
|
||||||
int iconY = fontY + (fontHeight - matchedEmote->height) / 2;
|
|
||||||
display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap);
|
display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap);
|
||||||
cursorX += matchedEmote->width + 1;
|
cursorX += matchedEmote->width + 1;
|
||||||
i += emojiLen;
|
i += emojiLen;
|
||||||
@@ -423,6 +405,63 @@ static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &
|
|||||||
return totalWidth;
|
return totalWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct MessageBlock {
|
||||||
|
size_t start;
|
||||||
|
size_t end;
|
||||||
|
bool mine;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool isHeaderLine)
|
||||||
|
{
|
||||||
|
if (isHeaderLine) {
|
||||||
|
return lineTopY + (FONT_HEIGHT_SMALL - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int tallest = FONT_HEIGHT_SMALL;
|
||||||
|
for (int e = 0; e < numEmotes; ++e) {
|
||||||
|
if (line.find(emotes[e].label) != std::string::npos) {
|
||||||
|
if (emotes[e].height > tallest)
|
||||||
|
tallest = emotes[e].height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int lineHeight = std::max(FONT_HEIGHT_SMALL, tallest);
|
||||||
|
const int iconTop = lineTopY + (lineHeight - tallest) / 2;
|
||||||
|
|
||||||
|
return iconTop + tallest - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<MessageBlock> buildMessageBlocks(const std::vector<bool> &isHeaderVec, const std::vector<bool> &isMineVec)
|
||||||
|
{
|
||||||
|
std::vector<MessageBlock> blocks;
|
||||||
|
if (isHeaderVec.empty())
|
||||||
|
return blocks;
|
||||||
|
|
||||||
|
size_t start = 0;
|
||||||
|
bool mine = isMineVec[0];
|
||||||
|
|
||||||
|
for (size_t i = 1; i < isHeaderVec.size(); ++i) {
|
||||||
|
if (isHeaderVec[i]) {
|
||||||
|
MessageBlock b;
|
||||||
|
b.start = start;
|
||||||
|
b.end = i - 1;
|
||||||
|
b.mine = mine;
|
||||||
|
blocks.push_back(b);
|
||||||
|
|
||||||
|
start = i;
|
||||||
|
mine = isMineVec[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageBlock last;
|
||||||
|
last.start = start;
|
||||||
|
last.end = isHeaderVec.size() - 1;
|
||||||
|
last.mine = mine;
|
||||||
|
blocks.push_back(last);
|
||||||
|
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int totalHeight, int scrollOffset, int startY)
|
static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int totalHeight, int scrollOffset, int startY)
|
||||||
{
|
{
|
||||||
if (totalHeight <= visibleHeight)
|
if (totalHeight <= visibleHeight)
|
||||||
@@ -482,9 +521,14 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
constexpr int LEFT_MARGIN = 2;
|
constexpr int LEFT_MARGIN = 2;
|
||||||
constexpr int RIGHT_MARGIN = 2;
|
constexpr int RIGHT_MARGIN = 2;
|
||||||
constexpr int SCROLLBAR_WIDTH = 3;
|
constexpr int SCROLLBAR_WIDTH = 3;
|
||||||
|
constexpr int BUBBLE_PAD_X = 3;
|
||||||
|
constexpr int BUBBLE_PAD_Y = 4;
|
||||||
|
constexpr int BUBBLE_RADIUS = 4;
|
||||||
|
constexpr int BUBBLE_MIN_W = 24;
|
||||||
|
constexpr int BUBBLE_TEXT_INDENT = 2;
|
||||||
|
|
||||||
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN;
|
// Derived widths
|
||||||
|
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (BUBBLE_PAD_X * 2);
|
||||||
const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH;
|
const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH;
|
||||||
|
|
||||||
// Title string depending on mode
|
// Title string depending on mode
|
||||||
@@ -547,7 +591,28 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
char chanType[32] = "";
|
char chanType[32] = "";
|
||||||
if (currentMode == ThreadMode::ALL) {
|
if (currentMode == ThreadMode::ALL) {
|
||||||
if (m.dest == NODENUM_BROADCAST) {
|
if (m.dest == NODENUM_BROADCAST) {
|
||||||
snprintf(chanType, sizeof(chanType), "#%s", channels.getName(m.channelIndex));
|
const char *name = channels.getName(m.channelIndex);
|
||||||
|
if (currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) {
|
||||||
|
if (strcmp(name, "ShortTurbo") == 0)
|
||||||
|
name = "ShortT";
|
||||||
|
else if (strcmp(name, "ShortSlow") == 0)
|
||||||
|
name = "ShortS";
|
||||||
|
else if (strcmp(name, "ShortFast") == 0)
|
||||||
|
name = "ShortF";
|
||||||
|
else if (strcmp(name, "MediumSlow") == 0)
|
||||||
|
name = "MedS";
|
||||||
|
else if (strcmp(name, "MediumFast") == 0)
|
||||||
|
name = "MedF";
|
||||||
|
else if (strcmp(name, "LongSlow") == 0)
|
||||||
|
name = "LongS";
|
||||||
|
else if (strcmp(name, "LongFast") == 0)
|
||||||
|
name = "LongF";
|
||||||
|
else if (strcmp(name, "LongTurbo") == 0)
|
||||||
|
name = "LongT";
|
||||||
|
else if (strcmp(name, "LongMod") == 0)
|
||||||
|
name = "LongM";
|
||||||
|
}
|
||||||
|
snprintf(chanType, sizeof(chanType), "#%s", name);
|
||||||
} else {
|
} else {
|
||||||
snprintf(chanType, sizeof(chanType), "(DM)");
|
snprintf(chanType, sizeof(chanType), "(DM)");
|
||||||
}
|
}
|
||||||
@@ -614,8 +679,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Shrink Sender name if needed
|
// Shrink Sender name if needed
|
||||||
int availWidth = SCREEN_WIDTH - display->getStringWidth(timeBuf) - display->getStringWidth(chanType) -
|
int availWidth = (mine ? rightTextWidth : leftTextWidth) - display->getStringWidth(timeBuf) -
|
||||||
display->getStringWidth(" @...") - 10;
|
display->getStringWidth(chanType) - display->getStringWidth(" @...");
|
||||||
if (availWidth < 0)
|
if (availWidth < 0)
|
||||||
availWidth = 0;
|
availWidth = 0;
|
||||||
|
|
||||||
@@ -667,6 +732,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
cachedLines = allLines;
|
cachedLines = allLines;
|
||||||
cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader);
|
cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader);
|
||||||
|
|
||||||
|
std::vector<MessageBlock> blocks = buildMessageBlocks(isHeader, isMine);
|
||||||
|
|
||||||
// Scrolling logic (unchanged)
|
// Scrolling logic (unchanged)
|
||||||
int totalHeight = 0;
|
int totalHeight = 0;
|
||||||
for (size_t i = 0; i < cachedHeights.size(); ++i)
|
for (size_t i = 0; i < cachedHeights.size(); ++i)
|
||||||
@@ -714,12 +781,133 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
|
|
||||||
int finalScroll = (int)scrollY;
|
int finalScroll = (int)scrollY;
|
||||||
int yOffset = -finalScroll + getTextPositions(display)[1];
|
int yOffset = -finalScroll + getTextPositions(display)[1];
|
||||||
|
const int contentTop = getTextPositions(display)[1];
|
||||||
|
const int contentBottom = scrollBottom; // already excludes nav line
|
||||||
|
const int rightEdge = SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN;
|
||||||
|
const int bubbleGapY = std::max(1, MESSAGE_BLOCK_GAP / 2);
|
||||||
|
|
||||||
|
std::vector<int> lineTop;
|
||||||
|
lineTop.resize(cachedLines.size());
|
||||||
|
{
|
||||||
|
int acc = 0;
|
||||||
|
for (size_t i = 0; i < cachedLines.size(); ++i) {
|
||||||
|
lineTop[i] = yOffset + acc;
|
||||||
|
acc += cachedHeights[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw bubbles
|
||||||
|
for (size_t bi = 0; bi < blocks.size(); ++bi) {
|
||||||
|
const auto &b = blocks[bi];
|
||||||
|
if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int visualTop = lineTop[b.start];
|
||||||
|
|
||||||
|
int topY;
|
||||||
|
if (isHeader[b.start]) {
|
||||||
|
// Header start
|
||||||
|
constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2
|
||||||
|
topY = visualTop - BUBBLE_PAD_TOP_HEADER;
|
||||||
|
} else {
|
||||||
|
// Body start
|
||||||
|
bool thisLineHasEmote = false;
|
||||||
|
for (int e = 0; e < numEmotes; ++e) {
|
||||||
|
if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
|
||||||
|
thisLineHasEmote = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (thisLineHasEmote) {
|
||||||
|
constexpr int EMOTE_PADDING_ABOVE = 4;
|
||||||
|
visualTop -= EMOTE_PADDING_ABOVE;
|
||||||
|
}
|
||||||
|
topY = visualTop - BUBBLE_PAD_Y;
|
||||||
|
}
|
||||||
|
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
|
||||||
|
int bottomY = visualBottom + BUBBLE_PAD_Y;
|
||||||
|
|
||||||
|
if (bi + 1 < blocks.size()) {
|
||||||
|
int nextHeaderIndex = (int)blocks[bi + 1].start;
|
||||||
|
int nextTop = lineTop[nextHeaderIndex];
|
||||||
|
int maxBottom = nextTop - 1 - bubbleGapY;
|
||||||
|
if (bottomY > maxBottom)
|
||||||
|
bottomY = maxBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bottomY <= topY + 2)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (bottomY < contentTop || topY > contentBottom - 1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int maxLineW = 0;
|
||||||
|
|
||||||
|
for (size_t i = b.start; i <= b.end; ++i) {
|
||||||
|
int w = 0;
|
||||||
|
if (isHeader[i]) {
|
||||||
|
w = display->getStringWidth(cachedLines[i].c_str());
|
||||||
|
if (b.mine)
|
||||||
|
w += 12; // room for ACK/NACK/relay mark
|
||||||
|
} else {
|
||||||
|
w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
|
||||||
|
}
|
||||||
|
if (w > maxLineW)
|
||||||
|
maxLineW = w;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (BUBBLE_PAD_X * 2));
|
||||||
|
int bubbleH = (bottomY - topY) + 1;
|
||||||
|
int bubbleX = 0;
|
||||||
|
if (b.mine) {
|
||||||
|
bubbleX = rightEdge - bubbleW;
|
||||||
|
} else {
|
||||||
|
bubbleX = x;
|
||||||
|
}
|
||||||
|
if (bubbleX < x)
|
||||||
|
bubbleX = x;
|
||||||
|
if (bubbleX + bubbleW > rightEdge)
|
||||||
|
bubbleW = std::max(1, rightEdge - bubbleX);
|
||||||
|
|
||||||
|
if (bubbleW > 1 && bubbleH > 1) {
|
||||||
|
int x1 = bubbleX + bubbleW - 1;
|
||||||
|
int y1 = topY + bubbleH - 1;
|
||||||
|
|
||||||
|
if (b.mine) {
|
||||||
|
// Send Message (Right side)
|
||||||
|
display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH);
|
||||||
|
// Top Right Corner
|
||||||
|
display->drawRect(x1, topY, 2, 1);
|
||||||
|
display->drawRect(x1, topY, 1, 2);
|
||||||
|
// Bottom Right Corner
|
||||||
|
display->drawRect(x1 - 1, bottomY - 2, 2, 1);
|
||||||
|
display->drawRect(x1, bottomY - 3, 1, 2);
|
||||||
|
// Knock the corners off to make a bubble
|
||||||
|
display->setColor(BLACK);
|
||||||
|
display->drawRect(x1 - bubbleW, topY - 1, 1, 1);
|
||||||
|
display->drawRect(x1 - bubbleW, bottomY - 1, 1, 1);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
} else {
|
||||||
|
// Received Message (Left Side)
|
||||||
|
display->drawRect(bubbleX, topY, bubbleW + 1, bubbleH);
|
||||||
|
// Top Left Corner
|
||||||
|
display->drawRect(bubbleX + 1, topY + 1, 2, 1);
|
||||||
|
display->drawRect(bubbleX + 1, topY + 1, 1, 2);
|
||||||
|
// Bottom Left Corner
|
||||||
|
display->drawRect(bubbleX + 1, bottomY - 1, 2, 1);
|
||||||
|
display->drawRect(bubbleX + 1, bottomY - 2, 1, 2);
|
||||||
|
// Knock the corners off to make a bubble
|
||||||
|
display->setColor(BLACK);
|
||||||
|
display->drawRect(bubbleX + bubbleW, topY, 1, 1);
|
||||||
|
display->drawRect(bubbleX + bubbleW, bottomY, 1, 1);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Render visible lines
|
// Render visible lines
|
||||||
|
int lineY = yOffset;
|
||||||
for (size_t i = 0; i < cachedLines.size(); ++i) {
|
for (size_t i = 0; i < cachedLines.size(); ++i) {
|
||||||
int lineY = yOffset;
|
|
||||||
for (size_t j = 0; j < i; ++j)
|
|
||||||
lineY += cachedHeights[j];
|
|
||||||
|
|
||||||
if (lineY > -cachedHeights[i] && lineY < scrollBottom) {
|
if (lineY > -cachedHeights[i] && lineY < scrollBottom) {
|
||||||
if (isHeader[i]) {
|
if (isHeader[i]) {
|
||||||
@@ -728,14 +916,28 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
int headerX;
|
int headerX;
|
||||||
if (isMine[i]) {
|
if (isMine[i]) {
|
||||||
// push header left to avoid overlap with scrollbar
|
// push header left to avoid overlap with scrollbar
|
||||||
headerX = SCREEN_WIDTH - w - SCROLLBAR_WIDTH - RIGHT_MARGIN;
|
headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - BUBBLE_TEXT_INDENT;
|
||||||
if (headerX < LEFT_MARGIN)
|
if (headerX < LEFT_MARGIN)
|
||||||
headerX = LEFT_MARGIN;
|
headerX = LEFT_MARGIN;
|
||||||
} else {
|
} else {
|
||||||
headerX = x;
|
headerX = x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT;
|
||||||
}
|
}
|
||||||
display->drawString(headerX, lineY, cachedLines[i].c_str());
|
display->drawString(headerX, lineY, cachedLines[i].c_str());
|
||||||
|
|
||||||
|
// Draw underline just under header text
|
||||||
|
int underlineY = lineY + FONT_HEIGHT_SMALL;
|
||||||
|
|
||||||
|
int underlineW = w;
|
||||||
|
int maxW = rightEdge - headerX;
|
||||||
|
if (maxW < 0)
|
||||||
|
maxW = 0;
|
||||||
|
if (underlineW > maxW)
|
||||||
|
underlineW = maxW;
|
||||||
|
|
||||||
|
for (int px = 0; px < underlineW; ++px) {
|
||||||
|
display->setPixel(headerX + px, underlineY);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw ACK/NACK mark for our own messages
|
// Draw ACK/NACK mark for our own messages
|
||||||
if (isMine[i]) {
|
if (isMine[i]) {
|
||||||
int markX = headerX - 10;
|
int markX = headerX - 10;
|
||||||
@@ -753,32 +955,28 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
// AckStatus::NONE → show nothing
|
// AckStatus::NONE → show nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw underline just under header text
|
|
||||||
int underlineY = lineY + FONT_HEIGHT_SMALL;
|
|
||||||
for (int px = 0; px < w; ++px) {
|
|
||||||
display->setPixel(headerX + px, underlineY);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Render message line
|
// Render message line
|
||||||
if (isMine[i]) {
|
if (isMine[i]) {
|
||||||
// Calculate actual rendered width including emotes
|
// Calculate actual rendered width including emotes
|
||||||
int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
|
int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
|
||||||
int rightX = SCREEN_WIDTH - renderedWidth - SCROLLBAR_WIDTH - RIGHT_MARGIN;
|
int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - BUBBLE_TEXT_INDENT;
|
||||||
if (rightX < LEFT_MARGIN)
|
if (rightX < LEFT_MARGIN)
|
||||||
rightX = LEFT_MARGIN;
|
rightX = LEFT_MARGIN;
|
||||||
|
|
||||||
drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes);
|
drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes);
|
||||||
} else {
|
} else {
|
||||||
drawStringWithEmotes(display, x, lineY, cachedLines[i], emotes, numEmotes);
|
drawStringWithEmotes(display, x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT, lineY, cachedLines[i], emotes,
|
||||||
|
numEmotes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lineY += cachedHeights[i];
|
||||||
}
|
}
|
||||||
int totalContentHeight = totalHeight;
|
|
||||||
int visibleHeight = usableHeight;
|
|
||||||
|
|
||||||
// Draw scrollbar
|
// Draw scrollbar
|
||||||
drawMessageScrollbar(display, visibleHeight, totalContentHeight, finalScroll, getTextPositions(display)[1]);
|
drawMessageScrollbar(display, usableHeight, totalHeight, finalScroll, getTextPositions(display)[1]);
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
graphics::drawCommonFooter(display, x, y);
|
graphics::drawCommonFooter(display, x, y);
|
||||||
}
|
}
|
||||||
@@ -841,7 +1039,6 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
|
|||||||
constexpr int HEADER_UNDERLINE_GAP = 0; // space between underline and first body line
|
constexpr int HEADER_UNDERLINE_GAP = 0; // space between underline and first body line
|
||||||
constexpr int HEADER_UNDERLINE_PIX = 1; // underline thickness (1px row drawn)
|
constexpr int HEADER_UNDERLINE_PIX = 1; // underline thickness (1px row drawn)
|
||||||
constexpr int BODY_LINE_LEADING = -4; // default vertical leading for normal body lines
|
constexpr int BODY_LINE_LEADING = -4; // default vertical leading for normal body lines
|
||||||
constexpr int MESSAGE_BLOCK_GAP = 4; // gap after a message block before a new header
|
|
||||||
constexpr int EMOTE_PADDING_ABOVE = 4; // space above emote line (added to line above)
|
constexpr int EMOTE_PADDING_ABOVE = 4; // space above emote line (added to line above)
|
||||||
constexpr int EMOTE_PADDING_BELOW = 3; // space below emote line (added to emote line)
|
constexpr int EMOTE_PADDING_BELOW = 3; // space below emote line (added to emote line)
|
||||||
|
|
||||||
@@ -851,6 +1048,7 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
|
|||||||
for (size_t idx = 0; idx < lines.size(); ++idx) {
|
for (size_t idx = 0; idx < lines.size(); ++idx) {
|
||||||
const auto &line = lines[idx];
|
const auto &line = lines[idx];
|
||||||
const int baseHeight = FONT_HEIGHT_SMALL;
|
const int baseHeight = FONT_HEIGHT_SMALL;
|
||||||
|
int lineHeight = baseHeight;
|
||||||
|
|
||||||
// Detect if THIS line or NEXT line contains an emote
|
// Detect if THIS line or NEXT line contains an emote
|
||||||
bool hasEmote = false;
|
bool hasEmote = false;
|
||||||
@@ -872,8 +1070,6 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int lineHeight = baseHeight;
|
|
||||||
|
|
||||||
if (isHeaderVec[idx]) {
|
if (isHeaderVec[idx]) {
|
||||||
// Header line spacing
|
// Header line spacing
|
||||||
lineHeight = baseHeight + HEADER_UNDERLINE_PIX + HEADER_UNDERLINE_GAP;
|
lineHeight = baseHeight + HEADER_UNDERLINE_PIX + HEADER_UNDERLINE_GAP;
|
||||||
@@ -922,7 +1118,7 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
|
|||||||
|
|
||||||
// Banner logic
|
// Banner logic
|
||||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from);
|
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from);
|
||||||
char longName[48] = "???";
|
char longName[48] = "?";
|
||||||
if (node && node->user.long_name) {
|
if (node && node->user.long_name) {
|
||||||
strncpy(longName, node->user.long_name, sizeof(longName) - 1);
|
strncpy(longName, node->user.long_name, sizeof(longName) - 1);
|
||||||
longName[sizeof(longName) - 1] = '\0';
|
longName[sizeof(longName) - 1] = '\0';
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ InkHUD::Tile *InkHUD::Applet::getTile()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw the applet
|
// Draw the applet
|
||||||
void InkHUD::Applet::render()
|
void InkHUD::Applet::render(bool full)
|
||||||
{
|
{
|
||||||
assert(assignedTile); // Ensure that we have a tile
|
assert(assignedTile); // Ensure that we have a tile
|
||||||
assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile
|
assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile
|
||||||
@@ -65,10 +65,11 @@ void InkHUD::Applet::render()
|
|||||||
wantRender = false; // Flag set by requestUpdate
|
wantRender = false; // Flag set by requestUpdate
|
||||||
wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored.
|
wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored.
|
||||||
wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted.
|
wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted.
|
||||||
|
wantFullRender = true; // Default to a full render
|
||||||
|
|
||||||
updateDimensions();
|
updateDimensions();
|
||||||
resetDrawingSpace();
|
resetDrawingSpace();
|
||||||
onRender(); // Derived applet's drawing takes place here
|
onRender(full); // Draw the applet
|
||||||
|
|
||||||
// Handle "Tile Highlighting"
|
// Handle "Tile Highlighting"
|
||||||
// Some devices may use an auxiliary button to switch between tiles
|
// Some devices may use an auxiliary button to switch between tiles
|
||||||
@@ -115,6 +116,11 @@ Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType()
|
|||||||
return wantUpdateType;
|
return wantUpdateType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool InkHUD::Applet::wantsFullRender()
|
||||||
|
{
|
||||||
|
return wantFullRender;
|
||||||
|
}
|
||||||
|
|
||||||
// Get size of the applet's drawing space from its tile
|
// Get size of the applet's drawing space from its tile
|
||||||
// Performed immediately before derived applet's drawing code runs
|
// Performed immediately before derived applet's drawing code runs
|
||||||
void InkHUD::Applet::updateDimensions()
|
void InkHUD::Applet::updateDimensions()
|
||||||
@@ -142,10 +148,11 @@ void InkHUD::Applet::resetDrawingSpace()
|
|||||||
// Once the renderer has given other applets a chance to process whatever event we just detected,
|
// Once the renderer has given other applets a chance to process whatever event we just detected,
|
||||||
// it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground)
|
// it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground)
|
||||||
// We should requestUpdate even if our applet is currently background, because this might be changed by autoshow
|
// We should requestUpdate even if our applet is currently background, because this might be changed by autoshow
|
||||||
void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type)
|
void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type, bool full)
|
||||||
{
|
{
|
||||||
wantRender = true;
|
wantRender = true;
|
||||||
wantUpdateType = type;
|
wantUpdateType = type;
|
||||||
|
wantFullRender = full;
|
||||||
inkhud->requestUpdate();
|
inkhud->requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,10 +64,11 @@ class Applet : public GFX
|
|||||||
|
|
||||||
// Rendering
|
// Rendering
|
||||||
|
|
||||||
void render(); // Draw the applet
|
void render(bool full); // Draw the applet
|
||||||
bool wantsToRender(); // Check whether applet wants to render
|
bool wantsToRender(); // Check whether applet wants to render
|
||||||
bool wantsToAutoshow(); // Check whether applet wants to become foreground
|
bool wantsToAutoshow(); // Check whether applet wants to become foreground
|
||||||
Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer
|
Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer
|
||||||
|
bool wantsFullRender(); // Check whether applet wants to render over its previous render
|
||||||
void updateDimensions(); // Get current size from tile
|
void updateDimensions(); // Get current size from tile
|
||||||
void resetDrawingSpace(); // Makes sure every render starts with same parameters
|
void resetDrawingSpace(); // Makes sure every render starts with same parameters
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ class Applet : public GFX
|
|||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
|
|
||||||
virtual void onRender() = 0; // All drawing happens here
|
virtual void onRender(bool full) = 0; // For drawing the applet
|
||||||
virtual void onActivate() {}
|
virtual void onActivate() {}
|
||||||
virtual void onDeactivate() {}
|
virtual void onDeactivate() {}
|
||||||
virtual void onForeground() {}
|
virtual void onForeground() {}
|
||||||
@@ -96,6 +97,9 @@ class Applet : public GFX
|
|||||||
virtual void onNavDown() {}
|
virtual void onNavDown() {}
|
||||||
virtual void onNavLeft() {}
|
virtual void onNavLeft() {}
|
||||||
virtual void onNavRight() {}
|
virtual void onNavRight() {}
|
||||||
|
virtual void onFreeText(char c) {}
|
||||||
|
virtual void onFreeTextDone() {}
|
||||||
|
virtual void onFreeTextCancel() {}
|
||||||
|
|
||||||
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
|
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
|
||||||
|
|
||||||
@@ -108,8 +112,9 @@ class Applet : public GFX
|
|||||||
protected:
|
protected:
|
||||||
void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here
|
void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here
|
||||||
|
|
||||||
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update
|
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED,
|
||||||
void requestAutoshow(); // Ask for applet to be moved to foreground
|
bool full = true); // Ask WindowManager to schedule a display update
|
||||||
|
void requestAutoshow(); // Ask for applet to be moved to foreground
|
||||||
|
|
||||||
uint16_t X(float f); // Map applet width, mapped from 0 to 1.0
|
uint16_t X(float f); // Map applet width, mapped from 0 to 1.0
|
||||||
uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0
|
uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0
|
||||||
@@ -164,6 +169,7 @@ class Applet : public GFX
|
|||||||
bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground?
|
bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground?
|
||||||
NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType =
|
NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType =
|
||||||
NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display
|
NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display
|
||||||
|
bool wantFullRender = true; // Render with a fresh canvas
|
||||||
|
|
||||||
using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly
|
using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly
|
||||||
using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager.
|
using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
using namespace NicheGraphics;
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
void InkHUD::MapApplet::onRender()
|
void InkHUD::MapApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
// Abort if no markers to render
|
// Abort if no markers to render
|
||||||
if (!enoughMarkers()) {
|
if (!enoughMarkers()) {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ namespace NicheGraphics::InkHUD
|
|||||||
class MapApplet : public Applet
|
class MapApplet : public Applet
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes
|
virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ uint8_t InkHUD::NodeListApplet::maxCards()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw, using info which derived applet placed into NodeListApplet::cards for us
|
// Draw, using info which derived applet placed into NodeListApplet::cards for us
|
||||||
void InkHUD::NodeListApplet::onRender()
|
void InkHUD::NodeListApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
|
|
||||||
// ================================
|
// ================================
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class NodeListApplet : public Applet, public MeshModule
|
|||||||
public:
|
public:
|
||||||
NodeListApplet(const char *name);
|
NodeListApplet(const char *name);
|
||||||
|
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
|
|
||||||
bool wantPacket(const meshtastic_MeshPacket *p) override;
|
bool wantPacket(const meshtastic_MeshPacket *p) override;
|
||||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using namespace NicheGraphics;
|
|||||||
|
|
||||||
// All drawing happens here
|
// All drawing happens here
|
||||||
// Our basic example doesn't do anything useful. It just passively prints some text.
|
// Our basic example doesn't do anything useful. It just passively prints some text.
|
||||||
void InkHUD::BasicExampleApplet::onRender()
|
void InkHUD::BasicExampleApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
printAt(0, 0, "Hello, World!");
|
printAt(0, 0, "Hello, World!");
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class BasicExampleApplet : public Applet
|
|||||||
// You must have an onRender() method
|
// You must have an onRender() method
|
||||||
// All drawing happens here
|
// All drawing happens here
|
||||||
|
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace NicheGraphics::InkHUD
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_Mesh
|
|||||||
// We can trigger a render by calling requestUpdate()
|
// We can trigger a render by calling requestUpdate()
|
||||||
// Render might be called by some external source
|
// Render might be called by some external source
|
||||||
// We should always be ready to draw
|
// We should always be ready to draw
|
||||||
void InkHUD::NewMsgExampleApplet::onRender()
|
void InkHUD::NewMsgExampleApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0)
|
printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0)
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class NewMsgExampleApplet : public Applet, public SinglePortModule
|
|||||||
NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {}
|
NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {}
|
||||||
|
|
||||||
// All drawing happens here
|
// All drawing happens here
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
|
|
||||||
// Your applet might also want to use some of these
|
// Your applet might also want to use some of these
|
||||||
// Useful for setting up or tidying up
|
// Useful for setting up or tidying up
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ InkHUD::AlignStickApplet::AlignStickApplet()
|
|||||||
bringToForeground();
|
bringToForeground();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::AlignStickApplet::onRender()
|
void InkHUD::AlignStickApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
setFont(fontMedium);
|
setFont(fontMedium);
|
||||||
printAt(0, 0, "Align Joystick:");
|
printAt(0, 0, "Align Joystick:");
|
||||||
@@ -152,19 +152,17 @@ void InkHUD::AlignStickApplet::onBackground()
|
|||||||
|
|
||||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::AlignStickApplet::onButtonLongPress()
|
void InkHUD::AlignStickApplet::onButtonLongPress()
|
||||||
{
|
{
|
||||||
sendToBackground();
|
sendToBackground();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::AlignStickApplet::onExitLong()
|
void InkHUD::AlignStickApplet::onExitLong()
|
||||||
{
|
{
|
||||||
sendToBackground();
|
sendToBackground();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::AlignStickApplet::onNavUp()
|
void InkHUD::AlignStickApplet::onNavUp()
|
||||||
@@ -172,7 +170,6 @@ void InkHUD::AlignStickApplet::onNavUp()
|
|||||||
settings->joystick.aligned = true;
|
settings->joystick.aligned = true;
|
||||||
|
|
||||||
sendToBackground();
|
sendToBackground();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::AlignStickApplet::onNavDown()
|
void InkHUD::AlignStickApplet::onNavDown()
|
||||||
@@ -181,7 +178,6 @@ void InkHUD::AlignStickApplet::onNavDown()
|
|||||||
settings->joystick.aligned = true;
|
settings->joystick.aligned = true;
|
||||||
|
|
||||||
sendToBackground();
|
sendToBackground();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::AlignStickApplet::onNavLeft()
|
void InkHUD::AlignStickApplet::onNavLeft()
|
||||||
@@ -190,7 +186,6 @@ void InkHUD::AlignStickApplet::onNavLeft()
|
|||||||
settings->joystick.aligned = true;
|
settings->joystick.aligned = true;
|
||||||
|
|
||||||
sendToBackground();
|
sendToBackground();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::AlignStickApplet::onNavRight()
|
void InkHUD::AlignStickApplet::onNavRight()
|
||||||
@@ -199,7 +194,6 @@ void InkHUD::AlignStickApplet::onNavRight()
|
|||||||
settings->joystick.aligned = true;
|
settings->joystick.aligned = true;
|
||||||
|
|
||||||
sendToBackground();
|
sendToBackground();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -23,7 +23,7 @@ class AlignStickApplet : public SystemApplet
|
|||||||
public:
|
public:
|
||||||
AlignStickApplet();
|
AlignStickApplet();
|
||||||
|
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
void onForeground() override;
|
void onForeground() override;
|
||||||
void onBackground() override;
|
void onBackground() override;
|
||||||
void onButtonLongPress() override;
|
void onButtonLongPress() override;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ using namespace NicheGraphics;
|
|||||||
|
|
||||||
InkHUD::BatteryIconApplet::BatteryIconApplet()
|
InkHUD::BatteryIconApplet::BatteryIconApplet()
|
||||||
{
|
{
|
||||||
|
alwaysRender = true; // render everytime the screen is updated
|
||||||
|
|
||||||
// Show at boot, if user has previously enabled the feature
|
// Show at boot, if user has previously enabled the feature
|
||||||
if (settings->optionalFeatures.batteryIcon)
|
if (settings->optionalFeatures.batteryIcon)
|
||||||
bringToForeground();
|
bringToForeground();
|
||||||
@@ -44,7 +46,7 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
|
|||||||
return 0; // Tell Observable to continue informing other observers
|
return 0; // Tell Observable to continue informing other observers
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::BatteryIconApplet::onRender()
|
void InkHUD::BatteryIconApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
// Fill entire tile
|
// Fill entire tile
|
||||||
// - size of icon controlled by size of tile
|
// - size of icon controlled by size of tile
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class BatteryIconApplet : public SystemApplet
|
|||||||
public:
|
public:
|
||||||
BatteryIconApplet();
|
BatteryIconApplet();
|
||||||
|
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available
|
int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
#include "./KeyboardApplet.h"
|
||||||
|
|
||||||
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
InkHUD::KeyboardApplet::KeyboardApplet()
|
||||||
|
{
|
||||||
|
// Calculate row widths
|
||||||
|
for (uint8_t row = 0; row < KBD_ROWS; row++) {
|
||||||
|
rowWidths[row] = 0;
|
||||||
|
for (uint8_t col = 0; col < KBD_COLS; col++)
|
||||||
|
rowWidths[row] += keyWidths[row * KBD_COLS + col];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::KeyboardApplet::onRender(bool full)
|
||||||
|
{
|
||||||
|
uint16_t em = fontSmall.lineHeight(); // 16 pt
|
||||||
|
uint16_t keyH = Y(1.0) / KBD_ROWS;
|
||||||
|
int16_t keyTopPadding = (keyH - fontSmall.lineHeight()) / 2;
|
||||||
|
|
||||||
|
if (full) { // Draw full keyboard
|
||||||
|
for (uint8_t row = 0; row < KBD_ROWS; row++) {
|
||||||
|
|
||||||
|
// Calculate the remaining space to be used as padding
|
||||||
|
int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
|
||||||
|
|
||||||
|
// Draw keys
|
||||||
|
uint16_t xPos = 0;
|
||||||
|
for (uint8_t col = 0; col < KBD_COLS; col++) {
|
||||||
|
Color fgcolor = BLACK;
|
||||||
|
uint8_t index = row * KBD_COLS + col;
|
||||||
|
uint16_t keyX = ((xPos * em) >> 4) + ((col * keyXPadding) / (KBD_COLS - 1));
|
||||||
|
uint16_t keyY = row * keyH;
|
||||||
|
uint16_t keyW = (keyWidths[index] * em) >> 4;
|
||||||
|
if (index == selectedKey) {
|
||||||
|
fgcolor = WHITE;
|
||||||
|
fillRect(keyX, keyY, keyW, keyH, BLACK);
|
||||||
|
}
|
||||||
|
drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[index], fgcolor);
|
||||||
|
xPos += keyWidths[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // Only draw the difference
|
||||||
|
if (selectedKey != prevSelectedKey) {
|
||||||
|
// Draw previously selected key
|
||||||
|
uint8_t row = prevSelectedKey / KBD_COLS;
|
||||||
|
int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
|
||||||
|
uint16_t xPos = 0;
|
||||||
|
for (uint8_t i = prevSelectedKey - (prevSelectedKey % KBD_COLS); i < prevSelectedKey; i++)
|
||||||
|
xPos += keyWidths[i];
|
||||||
|
uint16_t keyX = ((xPos * em) >> 4) + (((prevSelectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1));
|
||||||
|
uint16_t keyY = row * keyH;
|
||||||
|
uint16_t keyW = (keyWidths[prevSelectedKey] * em) >> 4;
|
||||||
|
fillRect(keyX, keyY, keyW, keyH, WHITE);
|
||||||
|
drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[prevSelectedKey], BLACK);
|
||||||
|
|
||||||
|
// Draw newly selected key
|
||||||
|
row = selectedKey / KBD_COLS;
|
||||||
|
keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
|
||||||
|
xPos = 0;
|
||||||
|
for (uint8_t i = selectedKey - (selectedKey % KBD_COLS); i < selectedKey; i++)
|
||||||
|
xPos += keyWidths[i];
|
||||||
|
keyX = ((xPos * em) >> 4) + (((selectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1));
|
||||||
|
keyY = row * keyH;
|
||||||
|
keyW = (keyWidths[selectedKey] * em) >> 4;
|
||||||
|
fillRect(keyX, keyY, keyW, keyH, BLACK);
|
||||||
|
drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[selectedKey], WHITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSelectedKey = selectedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the key label corresponding to the char
|
||||||
|
// for most keys it draws the character itself
|
||||||
|
// for ['\b', '\n', ' ', '\x1b'] it draws special glyphs
|
||||||
|
void InkHUD::KeyboardApplet::drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color)
|
||||||
|
{
|
||||||
|
if (key == '\b') {
|
||||||
|
// Draw backspace glyph: 13 x 9 px
|
||||||
|
/**
|
||||||
|
* [][][][][][][][][]
|
||||||
|
* [][] []
|
||||||
|
* [][] [] [] []
|
||||||
|
* [][] [] [] []
|
||||||
|
* [][] [] []
|
||||||
|
* [][] [] [] []
|
||||||
|
* [][] [] [] []
|
||||||
|
* [][] []
|
||||||
|
* [][][][][][][][][]
|
||||||
|
*/
|
||||||
|
const uint8_t bsBitmap[] = {0x0f, 0xf8, 0x18, 0x08, 0x32, 0x28, 0x61, 0x48, 0xc0,
|
||||||
|
0x88, 0x61, 0x48, 0x32, 0x28, 0x18, 0x08, 0x0f, 0xf8};
|
||||||
|
uint16_t leftPadding = (width - 13) >> 1;
|
||||||
|
drawBitmap(left + leftPadding, top + 1, bsBitmap, 13, 9, color);
|
||||||
|
} else if (key == '\n') {
|
||||||
|
// Draw done glyph: 12 x 9 px
|
||||||
|
/**
|
||||||
|
* [][]
|
||||||
|
* [][]
|
||||||
|
* [][]
|
||||||
|
* [][]
|
||||||
|
* [][]
|
||||||
|
* [][] [][]
|
||||||
|
* [][] [][]
|
||||||
|
* [][][]
|
||||||
|
* []
|
||||||
|
*/
|
||||||
|
const uint8_t doneBitmap[] = {0x00, 0x30, 0x00, 0x60, 0x00, 0xc0, 0x01, 0x80, 0x03,
|
||||||
|
0x00, 0xc6, 0x00, 0x6c, 0x00, 0x38, 0x00, 0x10, 0x00};
|
||||||
|
uint16_t leftPadding = (width - 12) >> 1;
|
||||||
|
drawBitmap(left + leftPadding, top + 1, doneBitmap, 12, 9, color);
|
||||||
|
} else if (key == ' ') {
|
||||||
|
// Draw space glyph: 13 x 9 px
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* [] []
|
||||||
|
* [] []
|
||||||
|
* [][][][][][][][][][][][][]
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const uint8_t spaceBitmap[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
|
||||||
|
0x08, 0x80, 0x08, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00};
|
||||||
|
uint16_t leftPadding = (width - 13) >> 1;
|
||||||
|
drawBitmap(left + leftPadding, top + 1, spaceBitmap, 13, 9, color);
|
||||||
|
} else if (key == '\x1b') {
|
||||||
|
setTextColor(color);
|
||||||
|
std::string keyText = "ESC";
|
||||||
|
uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1;
|
||||||
|
printAt(left + leftPadding, top, keyText);
|
||||||
|
} else {
|
||||||
|
setTextColor(color);
|
||||||
|
if (key >= 0x61)
|
||||||
|
key -= 32; // capitalize
|
||||||
|
std::string keyText = std::string(1, key);
|
||||||
|
uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1;
|
||||||
|
printAt(left + leftPadding, top, keyText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::KeyboardApplet::onForeground()
|
||||||
|
{
|
||||||
|
handleInput = true; // Intercept the button input for our applet
|
||||||
|
|
||||||
|
// Select the first key
|
||||||
|
selectedKey = 0;
|
||||||
|
prevSelectedKey = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::KeyboardApplet::onBackground()
|
||||||
|
{
|
||||||
|
handleInput = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::KeyboardApplet::onButtonShortPress()
|
||||||
|
{
|
||||||
|
char key = keys[selectedKey];
|
||||||
|
if (key == '\n') {
|
||||||
|
inkhud->freeTextDone();
|
||||||
|
inkhud->closeKeyboard();
|
||||||
|
} else if (key == '\x1b') {
|
||||||
|
inkhud->freeTextCancel();
|
||||||
|
inkhud->closeKeyboard();
|
||||||
|
} else {
|
||||||
|
inkhud->freeText(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::KeyboardApplet::onButtonLongPress()
|
||||||
|
{
|
||||||
|
char key = keys[selectedKey];
|
||||||
|
if (key == '\n') {
|
||||||
|
inkhud->freeTextDone();
|
||||||
|
inkhud->closeKeyboard();
|
||||||
|
} else if (key == '\x1b') {
|
||||||
|
inkhud->freeTextCancel();
|
||||||
|
inkhud->closeKeyboard();
|
||||||
|
} else {
|
||||||
|
if (key >= 0x61)
|
||||||
|
key -= 32; // capitalize
|
||||||
|
inkhud->freeText(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::KeyboardApplet::onExitShort()
|
||||||
|
{
|
||||||
|
inkhud->freeTextCancel();
|
||||||
|
inkhud->closeKeyboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::KeyboardApplet::onExitLong()
|
||||||
|
{
|
||||||
|
inkhud->freeTextCancel();
|
||||||
|
inkhud->closeKeyboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::KeyboardApplet::onNavUp()
|
||||||
|
{
|
||||||
|
if (selectedKey < KBD_COLS) // wrap
|
||||||
|
selectedKey += KBD_COLS * (KBD_ROWS - 1);
|
||||||
|
else // move 1 row back
|
||||||
|
selectedKey -= KBD_COLS;
|
||||||
|
|
||||||
|
// Request rendering over the previously drawn render
|
||||||
|
requestUpdate(EInk::UpdateTypes::FAST, false);
|
||||||
|
// Force an update to bypass lockRequests
|
||||||
|
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::KeyboardApplet::onNavDown()
|
||||||
|
{
|
||||||
|
selectedKey += KBD_COLS;
|
||||||
|
selectedKey %= (KBD_COLS * KBD_ROWS);
|
||||||
|
|
||||||
|
// Request rendering over the previously drawn render
|
||||||
|
requestUpdate(EInk::UpdateTypes::FAST, false);
|
||||||
|
// Force an update to bypass lockRequests
|
||||||
|
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::KeyboardApplet::onNavLeft()
|
||||||
|
{
|
||||||
|
if (selectedKey % KBD_COLS == 0) // wrap
|
||||||
|
selectedKey += KBD_COLS - 1;
|
||||||
|
else // move 1 column back
|
||||||
|
selectedKey--;
|
||||||
|
|
||||||
|
// Request rendering over the previously drawn render
|
||||||
|
requestUpdate(EInk::UpdateTypes::FAST, false);
|
||||||
|
// Force an update to bypass lockRequests
|
||||||
|
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::KeyboardApplet::onNavRight()
|
||||||
|
{
|
||||||
|
if (selectedKey % KBD_COLS == KBD_COLS - 1) // wrap
|
||||||
|
selectedKey -= KBD_COLS - 1;
|
||||||
|
else // move 1 column forward
|
||||||
|
selectedKey++;
|
||||||
|
|
||||||
|
// Request rendering over the previously drawn render
|
||||||
|
requestUpdate(EInk::UpdateTypes::FAST, false);
|
||||||
|
// Force an update to bypass lockRequests
|
||||||
|
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t InkHUD::KeyboardApplet::getKeyboardHeight()
|
||||||
|
{
|
||||||
|
const uint16_t keyH = fontSmall.lineHeight() * 1.2;
|
||||||
|
return keyH * KBD_ROWS;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
System Applet to render an on-screeen keyboard
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
#include "graphics/niche/InkHUD/InkHUD.h"
|
||||||
|
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||||
|
#include <string>
|
||||||
|
namespace NicheGraphics::InkHUD
|
||||||
|
{
|
||||||
|
|
||||||
|
class KeyboardApplet : public SystemApplet
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
KeyboardApplet();
|
||||||
|
|
||||||
|
void onRender(bool full) override;
|
||||||
|
void onForeground() override;
|
||||||
|
void onBackground() override;
|
||||||
|
void onButtonShortPress() override;
|
||||||
|
void onButtonLongPress() override;
|
||||||
|
void onExitShort() override;
|
||||||
|
void onExitLong() override;
|
||||||
|
void onNavUp() override;
|
||||||
|
void onNavDown() override;
|
||||||
|
void onNavLeft() override;
|
||||||
|
void onNavRight() override;
|
||||||
|
|
||||||
|
static uint16_t getKeyboardHeight(); // used to set the keyboard tile height
|
||||||
|
|
||||||
|
private:
|
||||||
|
void drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color);
|
||||||
|
|
||||||
|
static const uint8_t KBD_COLS = 11;
|
||||||
|
static const uint8_t KBD_ROWS = 4;
|
||||||
|
|
||||||
|
const char keys[KBD_COLS * KBD_ROWS] = {
|
||||||
|
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b', // row 0
|
||||||
|
'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n', // row 1
|
||||||
|
'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '!', ' ', // row 2
|
||||||
|
'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '?', '\x1b' // row 3
|
||||||
|
};
|
||||||
|
|
||||||
|
// This array represents the widths of each key in points
|
||||||
|
// 16 pt = line height of the text
|
||||||
|
const uint16_t keyWidths[KBD_COLS * KBD_ROWS] = {
|
||||||
|
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 0
|
||||||
|
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 1
|
||||||
|
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 2
|
||||||
|
16, 16, 16, 16, 16, 16, 16, 10, 10, 12, 40 // row 3
|
||||||
|
};
|
||||||
|
|
||||||
|
uint16_t rowWidths[KBD_ROWS];
|
||||||
|
uint8_t selectedKey = 0; // selected key index
|
||||||
|
uint8_t prevSelectedKey = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -30,7 +30,7 @@ InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet")
|
|||||||
// This is then drawn with a FULL refresh by Renderer::begin
|
// This is then drawn with a FULL refresh by Renderer::begin
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::LogoApplet::onRender()
|
void InkHUD::LogoApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
// Size of the region which the logo should "scale to fit"
|
// Size of the region which the logo should "scale to fit"
|
||||||
uint16_t logoWLimit = X(0.8);
|
uint16_t logoWLimit = X(0.8);
|
||||||
@@ -120,7 +120,7 @@ void InkHUD::LogoApplet::onBackground()
|
|||||||
|
|
||||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin displaying the screen which is shown at shutdown
|
// Begin displaying the screen which is shown at shutdown
|
||||||
@@ -138,10 +138,10 @@ void InkHUD::LogoApplet::onShutdown()
|
|||||||
// Intention is to restore display health.
|
// Intention is to restore display health.
|
||||||
|
|
||||||
inverted = true;
|
inverted = true;
|
||||||
inkhud->forceUpdate(Drivers::EInk::FULL, false);
|
inkhud->forceUpdate(Drivers::EInk::FULL, true, false);
|
||||||
delay(1000); // Cooldown. Back to back updates aren't great for health.
|
delay(1000); // Cooldown. Back to back updates aren't great for health.
|
||||||
inverted = false;
|
inverted = false;
|
||||||
inkhud->forceUpdate(Drivers::EInk::FULL, false);
|
inkhud->forceUpdate(Drivers::EInk::FULL, true, false);
|
||||||
delay(1000); // Cooldown
|
delay(1000); // Cooldown
|
||||||
|
|
||||||
// Prepare for the powered-off screen now
|
// Prepare for the powered-off screen now
|
||||||
@@ -155,6 +155,18 @@ void InkHUD::LogoApplet::onShutdown()
|
|||||||
// This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is complete
|
// This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is complete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InkHUD::LogoApplet::onApplyingChanges()
|
||||||
|
{
|
||||||
|
bringToForeground();
|
||||||
|
|
||||||
|
textLeft = "";
|
||||||
|
textRight = "";
|
||||||
|
textTitle = "Applying changes";
|
||||||
|
fontTitle = fontSmall;
|
||||||
|
|
||||||
|
inkhud->forceUpdate(Drivers::EInk::FAST, false);
|
||||||
|
}
|
||||||
|
|
||||||
void InkHUD::LogoApplet::onReboot()
|
void InkHUD::LogoApplet::onReboot()
|
||||||
{
|
{
|
||||||
bringToForeground();
|
bringToForeground();
|
||||||
@@ -164,7 +176,7 @@ void InkHUD::LogoApplet::onReboot()
|
|||||||
textTitle = "Rebooting...";
|
textTitle = "Rebooting...";
|
||||||
fontTitle = fontSmall;
|
fontTitle = fontSmall;
|
||||||
|
|
||||||
inkhud->forceUpdate(Drivers::EInk::FULL, false);
|
inkhud->forceUpdate(Drivers::EInk::FULL, true, false);
|
||||||
// Perform the update right now, waiting here until complete
|
// Perform the update right now, waiting here until complete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ class LogoApplet : public SystemApplet, public concurrency::OSThread
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
LogoApplet();
|
LogoApplet();
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
void onForeground() override;
|
void onForeground() override;
|
||||||
void onBackground() override;
|
void onBackground() override;
|
||||||
void onShutdown() override;
|
void onShutdown() override;
|
||||||
void onReboot() override;
|
void onReboot() override;
|
||||||
|
void onApplyingChanges();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int32_t runOnce() override;
|
int32_t runOnce() override;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace NicheGraphics::InkHUD
|
|||||||
enum MenuAction {
|
enum MenuAction {
|
||||||
NO_ACTION,
|
NO_ACTION,
|
||||||
SEND_PING,
|
SEND_PING,
|
||||||
|
FREE_TEXT,
|
||||||
STORE_CANNEDMESSAGE_SELECTION,
|
STORE_CANNEDMESSAGE_SELECTION,
|
||||||
SEND_CANNEDMESSAGE,
|
SEND_CANNEDMESSAGE,
|
||||||
SHUTDOWN,
|
SHUTDOWN,
|
||||||
@@ -36,6 +37,84 @@ enum MenuAction {
|
|||||||
TOGGLE_NOTIFICATIONS,
|
TOGGLE_NOTIFICATIONS,
|
||||||
TOGGLE_INVERT_COLOR,
|
TOGGLE_INVERT_COLOR,
|
||||||
TOGGLE_12H_CLOCK,
|
TOGGLE_12H_CLOCK,
|
||||||
|
// Regions
|
||||||
|
SET_REGION_US,
|
||||||
|
SET_REGION_EU_868,
|
||||||
|
SET_REGION_EU_433,
|
||||||
|
SET_REGION_CN,
|
||||||
|
SET_REGION_JP,
|
||||||
|
SET_REGION_ANZ,
|
||||||
|
SET_REGION_KR,
|
||||||
|
SET_REGION_TW,
|
||||||
|
SET_REGION_RU,
|
||||||
|
SET_REGION_IN,
|
||||||
|
SET_REGION_NZ_865,
|
||||||
|
SET_REGION_TH,
|
||||||
|
SET_REGION_LORA_24,
|
||||||
|
SET_REGION_UA_433,
|
||||||
|
SET_REGION_UA_868,
|
||||||
|
SET_REGION_MY_433,
|
||||||
|
SET_REGION_MY_919,
|
||||||
|
SET_REGION_SG_923,
|
||||||
|
SET_REGION_PH_433,
|
||||||
|
SET_REGION_PH_868,
|
||||||
|
SET_REGION_PH_915,
|
||||||
|
SET_REGION_ANZ_433,
|
||||||
|
SET_REGION_KZ_433,
|
||||||
|
SET_REGION_KZ_863,
|
||||||
|
SET_REGION_NP_865,
|
||||||
|
SET_REGION_BR_902,
|
||||||
|
// Device Roles
|
||||||
|
SET_ROLE_CLIENT,
|
||||||
|
SET_ROLE_CLIENT_MUTE,
|
||||||
|
SET_ROLE_ROUTER,
|
||||||
|
SET_ROLE_REPEATER,
|
||||||
|
// Presets
|
||||||
|
SET_PRESET_LONG_SLOW,
|
||||||
|
SET_PRESET_LONG_MODERATE,
|
||||||
|
SET_PRESET_LONG_FAST,
|
||||||
|
SET_PRESET_MEDIUM_SLOW,
|
||||||
|
SET_PRESET_MEDIUM_FAST,
|
||||||
|
SET_PRESET_SHORT_SLOW,
|
||||||
|
SET_PRESET_SHORT_FAST,
|
||||||
|
SET_PRESET_SHORT_TURBO,
|
||||||
|
// Timezones
|
||||||
|
SET_TZ_US_HAWAII,
|
||||||
|
SET_TZ_US_ALASKA,
|
||||||
|
SET_TZ_US_PACIFIC,
|
||||||
|
SET_TZ_US_ARIZONA,
|
||||||
|
SET_TZ_US_MOUNTAIN,
|
||||||
|
SET_TZ_US_CENTRAL,
|
||||||
|
SET_TZ_US_EASTERN,
|
||||||
|
SET_TZ_BR_BRAZILIA,
|
||||||
|
SET_TZ_UTC,
|
||||||
|
SET_TZ_EU_WESTERN,
|
||||||
|
SET_TZ_EU_CENTRAL,
|
||||||
|
SET_TZ_EU_EASTERN,
|
||||||
|
SET_TZ_ASIA_KOLKATA,
|
||||||
|
SET_TZ_ASIA_HONG_KONG,
|
||||||
|
SET_TZ_AU_AWST,
|
||||||
|
SET_TZ_AU_ACST,
|
||||||
|
SET_TZ_AU_AEST,
|
||||||
|
SET_TZ_PACIFIC_NZ,
|
||||||
|
// Power
|
||||||
|
TOGGLE_POWER_SAVE,
|
||||||
|
CALIBRATE_ADC,
|
||||||
|
// Bluetooth
|
||||||
|
TOGGLE_BLUETOOTH,
|
||||||
|
TOGGLE_BLUETOOTH_PAIR_MODE,
|
||||||
|
// Channel
|
||||||
|
TOGGLE_CHANNEL_UPLINK,
|
||||||
|
TOGGLE_CHANNEL_DOWNLINK,
|
||||||
|
TOGGLE_CHANNEL_POSITION,
|
||||||
|
SET_CHANNEL_PRECISION,
|
||||||
|
// Display
|
||||||
|
TOGGLE_DISPLAY_UNITS,
|
||||||
|
// Network
|
||||||
|
TOGGLE_WIFI,
|
||||||
|
// Administration
|
||||||
|
RESET_NODEDB_ALL,
|
||||||
|
RESET_NODEDB_KEEP_FAVORITES,
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace NicheGraphics::InkHUD
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -32,9 +32,13 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
|||||||
void onNavDown() override;
|
void onNavDown() override;
|
||||||
void onNavLeft() override;
|
void onNavLeft() override;
|
||||||
void onNavRight() override;
|
void onNavRight() override;
|
||||||
void onRender() override;
|
void onFreeText(char c) override;
|
||||||
|
void onFreeTextDone() override;
|
||||||
|
void onFreeTextCancel() override;
|
||||||
|
void onRender(bool full) override;
|
||||||
|
|
||||||
void show(Tile *t); // Open the menu, onto a user tile
|
void show(Tile *t); // Open the menu, onto a user tile
|
||||||
|
void setStartPage(MenuPage page);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton
|
Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton
|
||||||
@@ -50,20 +54,32 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
|||||||
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
|
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
|
||||||
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
||||||
|
|
||||||
|
void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height,
|
||||||
|
std::string text); // Draw input field for free text
|
||||||
uint16_t getSystemInfoPanelHeight();
|
uint16_t getSystemInfoPanelHeight();
|
||||||
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
|
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
|
||||||
uint16_t *height = nullptr); // Info panel at top of root menu
|
uint16_t *height = nullptr); // Info panel at top of root menu
|
||||||
void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh
|
void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh
|
||||||
void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data
|
void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data
|
||||||
|
|
||||||
|
MenuPage startPageOverride = MenuPage::ROOT;
|
||||||
MenuPage currentPage = MenuPage::ROOT;
|
MenuPage currentPage = MenuPage::ROOT;
|
||||||
MenuPage previousPage = MenuPage::EXIT;
|
MenuPage previousPage = MenuPage::EXIT;
|
||||||
uint8_t cursor = 0; // Which menu item is currently highlighted
|
uint8_t cursor = 0; // Which menu item is currently highlighted
|
||||||
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
|
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
|
||||||
|
bool freeTextMode = false;
|
||||||
uint16_t systemInfoPanelHeight = 0; // Need to know before we render
|
uint16_t systemInfoPanelHeight = 0; // Need to know before we render
|
||||||
|
uint16_t menuTextLimit = 200;
|
||||||
|
|
||||||
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
|
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
|
||||||
|
std::vector<std::string> nodeConfigLabels; // Persistent labels for Node Config pages
|
||||||
|
uint8_t selectedChannelIndex = 0; // Currently selected LoRa channel (Node Config → Radio → Channel)
|
||||||
|
bool channelPositionEnabled = false;
|
||||||
|
bool gpsEnabled = false;
|
||||||
|
|
||||||
|
// Recents menu checkbox state (derived from settings.recentlyActiveSeconds)
|
||||||
|
static constexpr uint8_t RECENTS_COUNT = 6;
|
||||||
|
bool recentsSelected[RECENTS_COUNT] = {};
|
||||||
|
|
||||||
// Data for selecting and sending canned messages via the menu
|
// Data for selecting and sending canned messages via the menu
|
||||||
// Placed into a sub-class for organization only
|
// Placed into a sub-class for organization only
|
||||||
@@ -94,6 +110,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
|||||||
// Cleared onBackground (when MenuApplet closes)
|
// Cleared onBackground (when MenuApplet closes)
|
||||||
std::vector<MessageItem> messageItems;
|
std::vector<MessageItem> messageItems;
|
||||||
std::vector<RecipientItem> recipientItems;
|
std::vector<RecipientItem> recipientItems;
|
||||||
|
|
||||||
|
MessageItem freeTextItem;
|
||||||
} cm;
|
} cm;
|
||||||
|
|
||||||
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
|
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class MenuItem
|
|||||||
MenuAction action = NO_ACTION;
|
MenuAction action = NO_ACTION;
|
||||||
MenuPage nextPage = EXIT;
|
MenuPage nextPage = EXIT;
|
||||||
bool *checkState = nullptr;
|
bool *checkState = nullptr;
|
||||||
|
bool isHeader = false; // Non-selectable section label
|
||||||
|
|
||||||
// Various constructors, depending on the intended function of the item
|
// Various constructors, depending on the intended function of the item
|
||||||
|
|
||||||
@@ -40,6 +41,12 @@ class MenuItem
|
|||||||
: label(label), action(action), nextPage(nextPage), checkState(checkState)
|
: label(label), action(action), nextPage(nextPage), checkState(checkState)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
static MenuItem Header(const char *label)
|
||||||
|
{
|
||||||
|
MenuItem item(label, NO_ACTION, EXIT);
|
||||||
|
item.isHeader = true;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace NicheGraphics::InkHUD
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|||||||
@@ -20,10 +20,27 @@ enum MenuPage : uint8_t {
|
|||||||
SEND,
|
SEND,
|
||||||
CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message
|
CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message
|
||||||
OPTIONS,
|
OPTIONS,
|
||||||
|
NODE_CONFIG,
|
||||||
|
NODE_CONFIG_LORA,
|
||||||
|
NODE_CONFIG_CHANNELS, // List of channels
|
||||||
|
NODE_CONFIG_CHANNEL_DETAIL, // Per-channel options
|
||||||
|
NODE_CONFIG_CHANNEL_PRECISION,
|
||||||
|
NODE_CONFIG_PRESET,
|
||||||
|
NODE_CONFIG_DEVICE,
|
||||||
|
NODE_CONFIG_DEVICE_ROLE,
|
||||||
|
NODE_CONFIG_POWER,
|
||||||
|
NODE_CONFIG_POWER_ADC_CAL,
|
||||||
|
NODE_CONFIG_NETWORK,
|
||||||
|
NODE_CONFIG_DISPLAY,
|
||||||
|
NODE_CONFIG_BLUETOOTH,
|
||||||
|
NODE_CONFIG_POSITION,
|
||||||
|
NODE_CONFIG_ADMIN_RESET,
|
||||||
|
TIMEZONE,
|
||||||
APPLETS,
|
APPLETS,
|
||||||
AUTOSHOW,
|
AUTOSHOW,
|
||||||
RECENTS, // Select length of "recentlyActiveSeconds"
|
RECENTS, // Select length of "recentlyActiveSeconds"
|
||||||
EXIT, // Dismiss the menu applet
|
REGION,
|
||||||
|
EXIT, // Dismiss the menu applet
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace NicheGraphics::InkHUD
|
} // namespace NicheGraphics::InkHUD
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::NotificationApplet::onRender()
|
void InkHUD::NotificationApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
// Clear the region beneath the tile
|
// Clear the region beneath the tile
|
||||||
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
||||||
@@ -139,54 +139,47 @@ void InkHUD::NotificationApplet::onForeground()
|
|||||||
void InkHUD::NotificationApplet::onBackground()
|
void InkHUD::NotificationApplet::onBackground()
|
||||||
{
|
{
|
||||||
handleInput = false;
|
handleInput = false;
|
||||||
|
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::NotificationApplet::onButtonShortPress()
|
void InkHUD::NotificationApplet::onButtonShortPress()
|
||||||
{
|
{
|
||||||
dismiss();
|
dismiss();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::NotificationApplet::onButtonLongPress()
|
void InkHUD::NotificationApplet::onButtonLongPress()
|
||||||
{
|
{
|
||||||
dismiss();
|
dismiss();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::NotificationApplet::onExitShort()
|
void InkHUD::NotificationApplet::onExitShort()
|
||||||
{
|
{
|
||||||
dismiss();
|
dismiss();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::NotificationApplet::onExitLong()
|
void InkHUD::NotificationApplet::onExitLong()
|
||||||
{
|
{
|
||||||
dismiss();
|
dismiss();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::NotificationApplet::onNavUp()
|
void InkHUD::NotificationApplet::onNavUp()
|
||||||
{
|
{
|
||||||
dismiss();
|
dismiss();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::NotificationApplet::onNavDown()
|
void InkHUD::NotificationApplet::onNavDown()
|
||||||
{
|
{
|
||||||
dismiss();
|
dismiss();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::NotificationApplet::onNavLeft()
|
void InkHUD::NotificationApplet::onNavLeft()
|
||||||
{
|
{
|
||||||
dismiss();
|
dismiss();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::NotificationApplet::onNavRight()
|
void InkHUD::NotificationApplet::onNavRight()
|
||||||
{
|
{
|
||||||
dismiss();
|
dismiss();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification
|
// Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class NotificationApplet : public SystemApplet
|
|||||||
public:
|
public:
|
||||||
NotificationApplet();
|
NotificationApplet();
|
||||||
|
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
void onForeground() override;
|
void onForeground() override;
|
||||||
void onBackground() override;
|
void onBackground() override;
|
||||||
void onButtonShortPress() override;
|
void onButtonShortPress() override;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ InkHUD::PairingApplet::PairingApplet()
|
|||||||
bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus);
|
bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::PairingApplet::onRender()
|
void InkHUD::PairingApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
// Header
|
// Header
|
||||||
setFont(fontMedium);
|
setFont(fontMedium);
|
||||||
@@ -45,7 +45,7 @@ void InkHUD::PairingApplet::onBackground()
|
|||||||
|
|
||||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status)
|
int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class PairingApplet : public SystemApplet
|
|||||||
public:
|
public:
|
||||||
PairingApplet();
|
PairingApplet();
|
||||||
|
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
void onForeground() override;
|
void onForeground() override;
|
||||||
void onBackground() override;
|
void onBackground() override;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
using namespace NicheGraphics;
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
void InkHUD::PlaceholderApplet::onRender()
|
void InkHUD::PlaceholderApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
// This placeholder applet fills its area with sparse diagonal lines
|
// This placeholder applet fills its area with sparse diagonal lines
|
||||||
hatchRegion(0, 0, width(), height(), 8, BLACK);
|
hatchRegion(0, 0, width(), height(), 8, BLACK);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace NicheGraphics::InkHUD
|
|||||||
class PlaceholderApplet : public SystemApplet
|
class PlaceholderApplet : public SystemApplet
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
|
|
||||||
// Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet.
|
// Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet.
|
||||||
// The window manager decides when and where it should be rendered
|
// The window manager decides when and where it should be rendered
|
||||||
|
|||||||
@@ -10,39 +10,42 @@ using namespace NicheGraphics;
|
|||||||
|
|
||||||
InkHUD::TipsApplet::TipsApplet()
|
InkHUD::TipsApplet::TipsApplet()
|
||||||
{
|
{
|
||||||
// Decide which tips (if any) should be shown to user after the boot screen
|
bool needsRegion = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET);
|
||||||
|
|
||||||
|
bool showTutorialTips = (settings->tips.firstBoot || needsRegion);
|
||||||
|
|
||||||
// Welcome screen
|
// Welcome screen
|
||||||
if (settings->tips.firstBoot)
|
if (showTutorialTips)
|
||||||
tipQueue.push_back(Tip::WELCOME);
|
tipQueue.push_back(Tip::WELCOME);
|
||||||
|
|
||||||
// Antenna, region, timezone
|
// Finish setup
|
||||||
// Shown at boot if region not yet set
|
if (needsRegion)
|
||||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
|
||||||
tipQueue.push_back(Tip::FINISH_SETUP);
|
tipQueue.push_back(Tip::FINISH_SETUP);
|
||||||
|
|
||||||
|
// Using the UI
|
||||||
|
if (showTutorialTips) {
|
||||||
|
tipQueue.push_back(Tip::CUSTOMIZATION);
|
||||||
|
tipQueue.push_back(Tip::BUTTONS);
|
||||||
|
}
|
||||||
|
|
||||||
// Shutdown info
|
// Shutdown info
|
||||||
// Shown until user performs one valid shutdown
|
// Shown until user performs one valid shutdown
|
||||||
if (!settings->tips.safeShutdownSeen)
|
if (!settings->tips.safeShutdownSeen)
|
||||||
tipQueue.push_back(Tip::SAFE_SHUTDOWN);
|
tipQueue.push_back(Tip::SAFE_SHUTDOWN);
|
||||||
|
|
||||||
// Using the UI
|
|
||||||
if (settings->tips.firstBoot) {
|
|
||||||
tipQueue.push_back(Tip::CUSTOMIZATION);
|
|
||||||
tipQueue.push_back(Tip::BUTTONS);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Catch an incorrect attempt at rotating display
|
// Catch an incorrect attempt at rotating display
|
||||||
if (config.display.flip_screen)
|
if (config.display.flip_screen)
|
||||||
tipQueue.push_back(Tip::ROTATION);
|
tipQueue.push_back(Tip::ROTATION);
|
||||||
|
|
||||||
// Applet is foreground immediately at boot, but is obscured by LogoApplet, which is also foreground
|
// Region picker
|
||||||
// LogoApplet can be considered to have a higher Z-index, because it is placed before TipsApplet in the systemApplets vector
|
if (needsRegion)
|
||||||
|
tipQueue.push_back(Tip::PICK_REGION);
|
||||||
|
|
||||||
if (!tipQueue.empty())
|
if (!tipQueue.empty())
|
||||||
bringToForeground();
|
bringToForeground();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::TipsApplet::onRender()
|
void InkHUD::TipsApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
switch (tipQueue.front()) {
|
switch (tipQueue.front()) {
|
||||||
case Tip::WELCOME:
|
case Tip::WELCOME:
|
||||||
@@ -51,81 +54,109 @@ void InkHUD::TipsApplet::onRender()
|
|||||||
|
|
||||||
case Tip::FINISH_SETUP: {
|
case Tip::FINISH_SETUP: {
|
||||||
setFont(fontMedium);
|
setFont(fontMedium);
|
||||||
printAt(0, 0, "Tip: Finish Setup");
|
const char *title = "Tip: Finish Setup";
|
||||||
|
uint16_t h = getWrappedTextHeight(0, width(), title);
|
||||||
|
printWrapped(0, 0, width(), title);
|
||||||
|
|
||||||
setFont(fontSmall);
|
setFont(fontSmall);
|
||||||
int16_t cursorY = fontMedium.lineHeight() * 1.5;
|
int16_t cursorY = h + fontSmall.lineHeight();
|
||||||
printAt(0, cursorY, "- connect antenna");
|
|
||||||
|
|
||||||
cursorY += fontSmall.lineHeight() * 1.2;
|
auto drawBullet = [&](const char *text) {
|
||||||
printAt(0, cursorY, "- connect a client app");
|
uint16_t bh = getWrappedTextHeight(0, width(), text);
|
||||||
|
printWrapped(0, cursorY, width(), text);
|
||||||
|
cursorY += bh + (fontSmall.lineHeight() / 3);
|
||||||
|
};
|
||||||
|
|
||||||
// Only if region not set
|
drawBullet("- connect antenna");
|
||||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
drawBullet("- connect a client app");
|
||||||
cursorY += fontSmall.lineHeight() * 1.2;
|
|
||||||
printAt(0, cursorY, "- set region");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only if tz not set
|
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
||||||
if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) {
|
drawBullet("- set region");
|
||||||
cursorY += fontSmall.lineHeight() * 1.2;
|
|
||||||
printAt(0, cursorY, "- set timezone");
|
|
||||||
}
|
|
||||||
|
|
||||||
cursorY += fontSmall.lineHeight() * 1.5;
|
if (!(*config.device.tzdef && config.device.tzdef[0] != 0))
|
||||||
printAt(0, cursorY, "More info at meshtastic.org");
|
drawBullet("- set timezone");
|
||||||
|
|
||||||
|
cursorY += fontSmall.lineHeight() / 2;
|
||||||
|
drawBullet("More info at meshtastic.org");
|
||||||
|
|
||||||
setFont(fontSmall);
|
|
||||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
|
case Tip::PICK_REGION: {
|
||||||
|
setFont(fontMedium);
|
||||||
|
printAt(0, 0, "Set Region");
|
||||||
|
|
||||||
|
setFont(fontSmall);
|
||||||
|
printWrapped(0, fontMedium.lineHeight() * 1.5, width(), "Please select your LoRa region to complete setup.");
|
||||||
|
|
||||||
|
printAt(0, Y(1.0), "Press button to choose", LEFT, BOTTOM);
|
||||||
|
} break;
|
||||||
|
|
||||||
case Tip::SAFE_SHUTDOWN: {
|
case Tip::SAFE_SHUTDOWN: {
|
||||||
setFont(fontMedium);
|
setFont(fontMedium);
|
||||||
printAt(0, 0, "Tip: Shutdown");
|
|
||||||
|
const char *title = "Tip: Shutdown";
|
||||||
|
uint16_t h = getWrappedTextHeight(0, width(), title);
|
||||||
|
printWrapped(0, 0, width(), title);
|
||||||
|
|
||||||
setFont(fontSmall);
|
setFont(fontSmall);
|
||||||
std::string shutdown;
|
int16_t cursorY = h + fontSmall.lineHeight();
|
||||||
shutdown += "Before removing power, please shut down from InkHUD menu, or a client app. \n";
|
|
||||||
shutdown += "\n";
|
const char *body = "Before removing power, please shut down from InkHUD menu, or a client app.\n\n"
|
||||||
shutdown += "This ensures data is saved.";
|
"This ensures data is saved.";
|
||||||
printWrapped(0, fontMedium.lineHeight() * 1.5, width(), shutdown);
|
|
||||||
|
uint16_t bodyH = getWrappedTextHeight(0, width(), body);
|
||||||
|
printWrapped(0, cursorY, width(), body);
|
||||||
|
cursorY += bodyH + (fontSmall.lineHeight() / 2);
|
||||||
|
|
||||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||||
|
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
case Tip::CUSTOMIZATION: {
|
case Tip::CUSTOMIZATION: {
|
||||||
setFont(fontMedium);
|
setFont(fontMedium);
|
||||||
printAt(0, 0, "Tip: Customization");
|
|
||||||
|
const char *title = "Tip: Customization";
|
||||||
|
uint16_t h = getWrappedTextHeight(0, width(), title);
|
||||||
|
printWrapped(0, 0, width(), title);
|
||||||
|
|
||||||
setFont(fontSmall);
|
setFont(fontSmall);
|
||||||
printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
|
int16_t cursorY = h + fontSmall.lineHeight();
|
||||||
"Configure & control display with the InkHUD menu. Optional features, layout, rotation, and more.");
|
|
||||||
|
const char *body = "Configure & control display with the InkHUD menu. "
|
||||||
|
"Optional features, layout, rotation, and more.";
|
||||||
|
|
||||||
|
uint16_t bodyH = getWrappedTextHeight(0, width(), body);
|
||||||
|
printWrapped(0, cursorY, width(), body);
|
||||||
|
cursorY += bodyH + (fontSmall.lineHeight() / 2);
|
||||||
|
|
||||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
case Tip::BUTTONS: {
|
case Tip::BUTTONS: {
|
||||||
setFont(fontMedium);
|
setFont(fontMedium);
|
||||||
printAt(0, 0, "Tip: Buttons");
|
|
||||||
|
const char *title = "Tip: Buttons";
|
||||||
|
uint16_t h = getWrappedTextHeight(0, width(), title);
|
||||||
|
printWrapped(0, 0, width(), title);
|
||||||
|
|
||||||
setFont(fontSmall);
|
setFont(fontSmall);
|
||||||
int16_t cursorY = fontMedium.lineHeight() * 1.5;
|
int16_t cursorY = h + fontSmall.lineHeight();
|
||||||
|
|
||||||
|
auto drawBullet = [&](const char *text) {
|
||||||
|
uint16_t bh = getWrappedTextHeight(0, width(), text);
|
||||||
|
printWrapped(0, cursorY, width(), text);
|
||||||
|
cursorY += bh + (fontSmall.lineHeight() / 3);
|
||||||
|
};
|
||||||
|
|
||||||
if (!settings->joystick.enabled) {
|
if (!settings->joystick.enabled) {
|
||||||
printAt(0, cursorY, "User Button");
|
drawBullet("User Button");
|
||||||
cursorY += fontSmall.lineHeight() * 1.2;
|
drawBullet("- short press: next");
|
||||||
printAt(0, cursorY, "- short press: next");
|
drawBullet("- long press: select or open menu");
|
||||||
cursorY += fontSmall.lineHeight() * 1.2;
|
|
||||||
printAt(0, cursorY, "- long press: select / open menu");
|
|
||||||
} else {
|
} else {
|
||||||
printAt(0, cursorY, "Joystick");
|
drawBullet("Joystick");
|
||||||
cursorY += fontSmall.lineHeight() * 1.2;
|
drawBullet("- press: open menu or select");
|
||||||
printAt(0, cursorY, "- open menu / select");
|
drawBullet("Exit Button");
|
||||||
cursorY += fontSmall.lineHeight() * 1.5;
|
drawBullet("- press: switch tile or close menu");
|
||||||
printAt(0, cursorY, "Exit Button");
|
|
||||||
cursorY += fontSmall.lineHeight() * 1.2;
|
|
||||||
printAt(0, cursorY, "- switch tile / close menu");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||||
@@ -133,12 +164,21 @@ void InkHUD::TipsApplet::onRender()
|
|||||||
|
|
||||||
case Tip::ROTATION: {
|
case Tip::ROTATION: {
|
||||||
setFont(fontMedium);
|
setFont(fontMedium);
|
||||||
printAt(0, 0, "Tip: Rotation");
|
|
||||||
|
const char *title = "Tip: Rotation";
|
||||||
|
uint16_t h = getWrappedTextHeight(0, width(), title);
|
||||||
|
printWrapped(0, 0, width(), title);
|
||||||
|
|
||||||
setFont(fontSmall);
|
setFont(fontSmall);
|
||||||
if (!settings->joystick.enabled) {
|
if (!settings->joystick.enabled) {
|
||||||
printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
|
int16_t cursorY = h + fontSmall.lineHeight();
|
||||||
"To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate.");
|
|
||||||
|
const char *body = "To rotate the display, use the InkHUD menu. "
|
||||||
|
"Long-press the user button > Options > Rotate.";
|
||||||
|
|
||||||
|
uint16_t bh = getWrappedTextHeight(0, width(), body);
|
||||||
|
printWrapped(0, cursorY, width(), body);
|
||||||
|
cursorY += bh + (fontSmall.lineHeight() / 2);
|
||||||
} else {
|
} else {
|
||||||
printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
|
printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
|
||||||
"To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate.");
|
"To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate.");
|
||||||
@@ -159,12 +199,15 @@ void InkHUD::TipsApplet::renderWelcome()
|
|||||||
{
|
{
|
||||||
uint16_t padW = X(0.05);
|
uint16_t padW = X(0.05);
|
||||||
|
|
||||||
|
// Detect portrait orientation
|
||||||
|
bool portrait = height() > width();
|
||||||
|
|
||||||
// Block 1 - logo & title
|
// Block 1 - logo & title
|
||||||
// ========================
|
// ========================
|
||||||
|
|
||||||
// Logo size
|
// Logo size
|
||||||
uint16_t logoWLimit = X(0.3);
|
uint16_t logoWLimit = portrait ? X(0.5) : X(0.3);
|
||||||
uint16_t logoHLimit = Y(0.3);
|
uint16_t logoHLimit = portrait ? Y(0.25) : Y(0.3);
|
||||||
uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit);
|
uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit);
|
||||||
uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit);
|
uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit);
|
||||||
|
|
||||||
@@ -177,7 +220,7 @@ void InkHUD::TipsApplet::renderWelcome()
|
|||||||
|
|
||||||
// Center the block
|
// Center the block
|
||||||
// Desired effect: equal margin from display edge for logo left and title right
|
// Desired effect: equal margin from display edge for logo left and title right
|
||||||
int16_t block1Y = Y(0.3);
|
int16_t block1Y = portrait ? Y(0.2) : Y(0.3);
|
||||||
int16_t block1CX = X(0.5) + (logoW / 2) - (titleW / 2);
|
int16_t block1CX = X(0.5) + (logoW / 2) - (titleW / 2);
|
||||||
int16_t logoCX = block1CX - (logoW / 2) - (padW / 2);
|
int16_t logoCX = block1CX - (logoW / 2) - (padW / 2);
|
||||||
int16_t titleCX = block1CX + (titleW / 2) + (padW / 2);
|
int16_t titleCX = block1CX + (titleW / 2) + (padW / 2);
|
||||||
@@ -192,7 +235,7 @@ void InkHUD::TipsApplet::renderWelcome()
|
|||||||
std::string subtitle = "InkHUD";
|
std::string subtitle = "InkHUD";
|
||||||
if (width() >= 200)
|
if (width() >= 200)
|
||||||
subtitle += " - A Heads-Up Display"; // Future proofing: narrower for tiny display
|
subtitle += " - A Heads-Up Display"; // Future proofing: narrower for tiny display
|
||||||
printAt(X(0.5), Y(0.6), subtitle, CENTER, MIDDLE);
|
printAt(X(0.5), portrait ? Y(0.45) : Y(0.6), subtitle, CENTER, MIDDLE);
|
||||||
|
|
||||||
// Block 3 - press to continue
|
// Block 3 - press to continue
|
||||||
// ============================
|
// ============================
|
||||||
@@ -218,32 +261,42 @@ void InkHUD::TipsApplet::onBackground()
|
|||||||
|
|
||||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// While our SystemApplet::handleInput flag is true
|
// While our SystemApplet::handleInput flag is true
|
||||||
void InkHUD::TipsApplet::onButtonShortPress()
|
void InkHUD::TipsApplet::onButtonShortPress()
|
||||||
{
|
{
|
||||||
|
bool needsRegion = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET);
|
||||||
|
// If we're prompting the user to pick a region, hand off to the menu
|
||||||
|
if (!tipQueue.empty() && tipQueue.front() == Tip::PICK_REGION) {
|
||||||
|
tipQueue.pop_front();
|
||||||
|
|
||||||
|
// Signal InkHUD to open the menu on Region page
|
||||||
|
inkhud->forceRegionMenu = true;
|
||||||
|
|
||||||
|
// Close tips and open menu
|
||||||
|
sendToBackground();
|
||||||
|
inkhud->openMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Consume current tip
|
||||||
tipQueue.pop_front();
|
tipQueue.pop_front();
|
||||||
|
|
||||||
// All tips done
|
// All tips done
|
||||||
if (tipQueue.empty()) {
|
if (tipQueue.empty()) {
|
||||||
// Record that user has now seen the "tutorial" set of tips
|
// Record that user has now seen the "tutorial" set of tips
|
||||||
// Don't show them on subsequent boots
|
// Don't show them on subsequent boots
|
||||||
if (settings->tips.firstBoot) {
|
if (settings->tips.firstBoot && !needsRegion) {
|
||||||
settings->tips.firstBoot = false;
|
settings->tips.firstBoot = false;
|
||||||
inkhud->persistence->saveSettings();
|
inkhud->persistence->saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close applet, and full refresh to clean the screen
|
// Close applet
|
||||||
// Need to force update, because our request would be ignored otherwise, as we are now background
|
|
||||||
sendToBackground();
|
sendToBackground();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
// More tips left
|
|
||||||
else
|
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Functions the same as the user button in this instance
|
// Functions the same as the user button in this instance
|
||||||
@@ -252,4 +305,4 @@ void InkHUD::TipsApplet::onExitShort()
|
|||||||
onButtonShortPress();
|
onButtonShortPress();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class TipsApplet : public SystemApplet
|
|||||||
enum class Tip {
|
enum class Tip {
|
||||||
WELCOME,
|
WELCOME,
|
||||||
FINISH_SETUP,
|
FINISH_SETUP,
|
||||||
|
PICK_REGION,
|
||||||
SAFE_SHUTDOWN,
|
SAFE_SHUTDOWN,
|
||||||
CUSTOMIZATION,
|
CUSTOMIZATION,
|
||||||
BUTTONS,
|
BUTTONS,
|
||||||
@@ -32,7 +33,7 @@ class TipsApplet : public SystemApplet
|
|||||||
public:
|
public:
|
||||||
TipsApplet();
|
TipsApplet();
|
||||||
|
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
void onForeground() override;
|
void onForeground() override;
|
||||||
void onBackground() override;
|
void onBackground() override;
|
||||||
void onButtonShortPress() override;
|
void onButtonShortPress() override;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::AllMessageApplet::onRender()
|
void InkHUD::AllMessageApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
// Find newest message, regardless of whether DM or broadcast
|
// Find newest message, regardless of whether DM or broadcast
|
||||||
MessageStore::Message *message;
|
MessageStore::Message *message;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class Applet;
|
|||||||
class AllMessageApplet : public Applet
|
class AllMessageApplet : public Applet
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
|
|
||||||
void onActivate() override;
|
void onActivate() override;
|
||||||
void onDeactivate() override;
|
void onDeactivate() override;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::DMApplet::onRender()
|
void InkHUD::DMApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
// Abort if no text message
|
// Abort if no text message
|
||||||
if (!latestMessage->dm.sender) {
|
if (!latestMessage->dm.sender) {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class Applet;
|
|||||||
class DMApplet : public Applet
|
class DMApplet : public Applet
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
|
|
||||||
void onActivate() override;
|
void onActivate() override;
|
||||||
void onDeactivate() override;
|
void onDeactivate() override;
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
|
|
||||||
using namespace NicheGraphics;
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
void InkHUD::PositionsApplet::onRender()
|
void InkHUD::PositionsApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
// Draw the usual map applet first
|
// Draw the usual map applet first
|
||||||
MapApplet::onRender();
|
MapApplet::onRender(full);
|
||||||
|
|
||||||
// Draw our latest "node of interest" as a special marker
|
// Draw our latest "node of interest" as a special marker
|
||||||
// -------------------------------------------------------
|
// -------------------------------------------------------
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class PositionsApplet : public MapApplet, public SinglePortModule
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {}
|
PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {}
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex)
|
|||||||
store = new MessageStore("ch" + to_string(channelIndex));
|
store = new MessageStore("ch" + to_string(channelIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::ThreadedMessageApplet::onRender()
|
void InkHUD::ThreadedMessageApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
// =============
|
// =============
|
||||||
// Draw a header
|
// Draw a header
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class ThreadedMessageApplet : public Applet, public SinglePortModule
|
|||||||
explicit ThreadedMessageApplet(uint8_t channelIndex);
|
explicit ThreadedMessageApplet(uint8_t channelIndex);
|
||||||
ThreadedMessageApplet() = delete;
|
ThreadedMessageApplet() = delete;
|
||||||
|
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
|
|
||||||
void onActivate() override;
|
void onActivate() override;
|
||||||
void onDeactivate() override;
|
void onDeactivate() override;
|
||||||
|
|||||||
@@ -238,6 +238,39 @@ void InkHUD::Events::onNavRight()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InkHUD::Events::onFreeText(char c)
|
||||||
|
{
|
||||||
|
// Trigger the first system applet that wants to handle the new character
|
||||||
|
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||||
|
if (sa->handleFreeText) {
|
||||||
|
sa->onFreeText(c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::Events::onFreeTextDone()
|
||||||
|
{
|
||||||
|
// Trigger the first system applet that wants to handle it
|
||||||
|
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||||
|
if (sa->handleFreeText) {
|
||||||
|
sa->onFreeTextDone();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::Events::onFreeTextCancel()
|
||||||
|
{
|
||||||
|
// Trigger the first system applet that wants to handle it
|
||||||
|
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||||
|
if (sa->handleFreeText) {
|
||||||
|
sa->onFreeTextCancel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Callback for deepSleepObserver
|
// Callback for deepSleepObserver
|
||||||
// Returns 0 to signal that we agree to sleep now
|
// Returns 0 to signal that we agree to sleep now
|
||||||
int InkHUD::Events::beforeDeepSleep(void *unused)
|
int InkHUD::Events::beforeDeepSleep(void *unused)
|
||||||
@@ -266,7 +299,7 @@ int InkHUD::Events::beforeDeepSleep(void *unused)
|
|||||||
// then prepared a final powered-off screen for us, which shows device shortname.
|
// then prepared a final powered-off screen for us, which shows device shortname.
|
||||||
// We're updating to show that one now.
|
// We're updating to show that one now.
|
||||||
|
|
||||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
|
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false);
|
||||||
delay(1000); // Cooldown, before potentially yanking display power
|
delay(1000); // Cooldown, before potentially yanking display power
|
||||||
|
|
||||||
// InkHUD shutdown complete
|
// InkHUD shutdown complete
|
||||||
@@ -276,6 +309,15 @@ int InkHUD::Events::beforeDeepSleep(void *unused)
|
|||||||
return 0; // We agree: deep sleep now
|
return 0; // We agree: deep sleep now
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display an intermediate screen while configuration changes are applied
|
||||||
|
void InkHUD::Events::applyingChanges()
|
||||||
|
{
|
||||||
|
// Bring the logo applet forward with a temporary message
|
||||||
|
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||||
|
sa->onApplyingChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Callback for rebootObserver
|
// Callback for rebootObserver
|
||||||
// Same as shutdown, without drawing the logoApplet
|
// Same as shutdown, without drawing the logoApplet
|
||||||
// Makes sure we don't lose message history / InkHUD config
|
// Makes sure we don't lose message history / InkHUD config
|
||||||
|
|||||||
@@ -29,12 +29,18 @@ class Events
|
|||||||
|
|
||||||
void onButtonShort(); // User button: short press
|
void onButtonShort(); // User button: short press
|
||||||
void onButtonLong(); // User button: long press
|
void onButtonLong(); // User button: long press
|
||||||
void onExitShort(); // Exit button: short press
|
void applyingChanges();
|
||||||
void onExitLong(); // Exit button: long press
|
void onExitShort(); // Exit button: short press
|
||||||
void onNavUp(); // Navigate up
|
void onExitLong(); // Exit button: long press
|
||||||
void onNavDown(); // Navigate down
|
void onNavUp(); // Navigate up
|
||||||
void onNavLeft(); // Navigate left
|
void onNavDown(); // Navigate down
|
||||||
void onNavRight(); // Navigate right
|
void onNavLeft(); // Navigate left
|
||||||
|
void onNavRight(); // Navigate right
|
||||||
|
|
||||||
|
// Free text typing events
|
||||||
|
void onFreeText(char c); // New freetext character input
|
||||||
|
void onFreeTextDone();
|
||||||
|
void onFreeTextCancel();
|
||||||
|
|
||||||
int beforeDeepSleep(void *unused); // Prepare for shutdown
|
int beforeDeepSleep(void *unused); // Prepare for shutdown
|
||||||
int beforeReboot(void *unused); // Prepare for reboot
|
int beforeReboot(void *unused); // Prepare for reboot
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ void InkHUD::InkHUD::addApplet(const char *name, Applet *a, bool defaultActive,
|
|||||||
windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile);
|
windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InkHUD::InkHUD::notifyApplyingChanges()
|
||||||
|
{
|
||||||
|
if (events) {
|
||||||
|
events->applyingChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start InkHUD!
|
// Start InkHUD!
|
||||||
// Call this only after you have configured InkHUD
|
// Call this only after you have configured InkHUD
|
||||||
void InkHUD::InkHUD::begin()
|
void InkHUD::InkHUD::begin()
|
||||||
@@ -168,6 +175,25 @@ void InkHUD::InkHUD::navRight()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call this for keyboard input
|
||||||
|
// The Keyboard Applet also calls this
|
||||||
|
void InkHUD::InkHUD::freeText(char c)
|
||||||
|
{
|
||||||
|
events->onFreeText(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call this to complete a freetext input
|
||||||
|
void InkHUD::InkHUD::freeTextDone()
|
||||||
|
{
|
||||||
|
events->onFreeTextDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call this to cancel a freetext input
|
||||||
|
void InkHUD::InkHUD::freeTextCancel()
|
||||||
|
{
|
||||||
|
events->onFreeTextCancel();
|
||||||
|
}
|
||||||
|
|
||||||
// Cycle the next user applet to the foreground
|
// Cycle the next user applet to the foreground
|
||||||
// Only activated applets are cycled
|
// Only activated applets are cycled
|
||||||
// If user has a multi-applet layout, the applets will cycle on the "focused tile"
|
// If user has a multi-applet layout, the applets will cycle on the "focused tile"
|
||||||
@@ -197,6 +223,18 @@ void InkHUD::InkHUD::openAlignStick()
|
|||||||
windowManager->openAlignStick();
|
windowManager->openAlignStick();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open the on-screen keyboard
|
||||||
|
void InkHUD::InkHUD::openKeyboard()
|
||||||
|
{
|
||||||
|
windowManager->openKeyboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the on-screen keyboard
|
||||||
|
void InkHUD::InkHUD::closeKeyboard()
|
||||||
|
{
|
||||||
|
windowManager->closeKeyboard();
|
||||||
|
}
|
||||||
|
|
||||||
// In layouts where multiple applets are shown at once, change which tile is focused
|
// In layouts where multiple applets are shown at once, change which tile is focused
|
||||||
// The focused tile in the one which cycles applets on button short press, and displays menu on long press
|
// The focused tile in the one which cycles applets on button short press, and displays menu on long press
|
||||||
void InkHUD::InkHUD::nextTile()
|
void InkHUD::InkHUD::nextTile()
|
||||||
@@ -245,10 +283,11 @@ void InkHUD::InkHUD::requestUpdate()
|
|||||||
// Ignores all diplomacy:
|
// Ignores all diplomacy:
|
||||||
// - the display *will* update
|
// - the display *will* update
|
||||||
// - the specified update type *will* be used
|
// - the specified update type *will* be used
|
||||||
|
// If the all parameter is true, the whole screen buffer is cleared and re-rendered
|
||||||
// If the async parameter is false, code flow is blocked while the update takes place
|
// If the async parameter is false, code flow is blocked while the update takes place
|
||||||
void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool async)
|
void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool all, bool async)
|
||||||
{
|
{
|
||||||
renderer->forceUpdate(type, async);
|
renderer->forceUpdate(type, all, async);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for any in-progress display update to complete before continuing
|
// Wait for any in-progress display update to complete before continuing
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class InkHUD
|
|||||||
void setDriver(Drivers::EInk *driver);
|
void setDriver(Drivers::EInk *driver);
|
||||||
void setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0);
|
void setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0);
|
||||||
void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, uint8_t onTile = -1);
|
void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, uint8_t onTile = -1);
|
||||||
|
void notifyApplyingChanges();
|
||||||
|
|
||||||
void begin();
|
void begin();
|
||||||
|
|
||||||
@@ -62,6 +63,11 @@ class InkHUD
|
|||||||
void navLeft();
|
void navLeft();
|
||||||
void navRight();
|
void navRight();
|
||||||
|
|
||||||
|
// Freetext handlers
|
||||||
|
void freeText(char c);
|
||||||
|
void freeTextDone();
|
||||||
|
void freeTextCancel();
|
||||||
|
|
||||||
// Trigger UI changes
|
// Trigger UI changes
|
||||||
// - called by various InkHUD components
|
// - called by various InkHUD components
|
||||||
// - suitable(?) for use by aux button, connected in variant nicheGraphics.h
|
// - suitable(?) for use by aux button, connected in variant nicheGraphics.h
|
||||||
@@ -70,17 +76,23 @@ class InkHUD
|
|||||||
void prevApplet();
|
void prevApplet();
|
||||||
void openMenu();
|
void openMenu();
|
||||||
void openAlignStick();
|
void openAlignStick();
|
||||||
|
void openKeyboard();
|
||||||
|
void closeKeyboard();
|
||||||
void nextTile();
|
void nextTile();
|
||||||
void prevTile();
|
void prevTile();
|
||||||
void rotate();
|
void rotate();
|
||||||
void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default
|
void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default
|
||||||
void toggleBatteryIcon();
|
void toggleBatteryIcon();
|
||||||
|
|
||||||
|
// Used by TipsApplet to force menu to start on Region selection
|
||||||
|
bool forceRegionMenu = false;
|
||||||
|
|
||||||
// Updating the display
|
// Updating the display
|
||||||
// - called by various InkHUD components
|
// - called by various InkHUD components
|
||||||
|
|
||||||
void requestUpdate();
|
void requestUpdate();
|
||||||
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool async = true);
|
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool all = false,
|
||||||
|
bool async = true);
|
||||||
void awaitUpdate();
|
void awaitUpdate();
|
||||||
|
|
||||||
// (Re)configuring WindowManager
|
// (Re)configuring WindowManager
|
||||||
|
|||||||
@@ -56,15 +56,16 @@ void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMul
|
|||||||
|
|
||||||
void InkHUD::Renderer::begin()
|
void InkHUD::Renderer::begin()
|
||||||
{
|
{
|
||||||
forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
|
forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a flag, which will be picked up by runOnce, ASAP.
|
// Set a flag, which will be picked up by runOnce, ASAP.
|
||||||
// Quite likely, multiple applets will all want to respond to one event (Observable, etc)
|
// Quite likely, multiple applets will all want to respond to one event (Observable, etc)
|
||||||
// Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next runOnce
|
// Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next runOnce
|
||||||
void InkHUD::Renderer::requestUpdate()
|
void InkHUD::Renderer::requestUpdate(bool all)
|
||||||
{
|
{
|
||||||
requested = true;
|
requested = true;
|
||||||
|
renderAll |= all;
|
||||||
|
|
||||||
// We will run the thread as soon as we loop(),
|
// We will run the thread as soon as we loop(),
|
||||||
// after all Applets have had a chance to observe whatever event set this off
|
// after all Applets have had a chance to observe whatever event set this off
|
||||||
@@ -79,10 +80,11 @@ void InkHUD::Renderer::requestUpdate()
|
|||||||
// Sometimes, however, we will want to trigger a display update manually, in the absence of any sort of applet event
|
// Sometimes, however, we will want to trigger a display update manually, in the absence of any sort of applet event
|
||||||
// Display health, for example.
|
// Display health, for example.
|
||||||
// In these situations, we use forceUpdate
|
// In these situations, we use forceUpdate
|
||||||
void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool async)
|
void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool all, bool async)
|
||||||
{
|
{
|
||||||
requested = true;
|
requested = true;
|
||||||
forced = true;
|
forced = true;
|
||||||
|
renderAll |= all;
|
||||||
displayHealth.forceUpdateType(type);
|
displayHealth.forceUpdateType(type);
|
||||||
|
|
||||||
// Normally, we need to start the timer, in case the display is busy and we briefly defer the update
|
// Normally, we need to start the timer, in case the display is busy and we briefly defer the update
|
||||||
@@ -219,7 +221,8 @@ void InkHUD::Renderer::render(bool async)
|
|||||||
Drivers::EInk::UpdateTypes updateType = decideUpdateType();
|
Drivers::EInk::UpdateTypes updateType = decideUpdateType();
|
||||||
|
|
||||||
// Render the new image
|
// Render the new image
|
||||||
clearBuffer();
|
if (renderAll)
|
||||||
|
clearBuffer();
|
||||||
renderUserApplets();
|
renderUserApplets();
|
||||||
renderPlaceholders();
|
renderPlaceholders();
|
||||||
renderSystemApplets();
|
renderSystemApplets();
|
||||||
@@ -247,6 +250,7 @@ void InkHUD::Renderer::render(bool async)
|
|||||||
// Tidy up, ready for a new request
|
// Tidy up, ready for a new request
|
||||||
requested = false;
|
requested = false;
|
||||||
forced = false;
|
forced = false;
|
||||||
|
renderAll = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manually fill the image buffer with WHITE
|
// Manually fill the image buffer with WHITE
|
||||||
@@ -259,6 +263,76 @@ void InkHUD::Renderer::clearBuffer()
|
|||||||
memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth);
|
memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manually clear the pixels below a tile
|
||||||
|
void InkHUD::Renderer::clearTile(Tile *t)
|
||||||
|
{
|
||||||
|
// Rotate the tile dimensions
|
||||||
|
int16_t left = 0;
|
||||||
|
int16_t top = 0;
|
||||||
|
uint16_t width = 0;
|
||||||
|
uint16_t height = 0;
|
||||||
|
switch (settings->rotation) {
|
||||||
|
case 0:
|
||||||
|
left = t->getLeft();
|
||||||
|
top = t->getTop();
|
||||||
|
width = t->getWidth();
|
||||||
|
height = t->getHeight();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
left = driver->width - (t->getTop() + t->getHeight());
|
||||||
|
top = t->getLeft();
|
||||||
|
width = t->getHeight();
|
||||||
|
height = t->getWidth();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
left = driver->width - (t->getLeft() + t->getWidth());
|
||||||
|
top = driver->height - (t->getTop() + t->getHeight());
|
||||||
|
width = t->getWidth();
|
||||||
|
height = t->getHeight();
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
left = t->getTop();
|
||||||
|
top = driver->height - (t->getLeft() + t->getWidth());
|
||||||
|
width = t->getHeight();
|
||||||
|
height = t->getWidth();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the bounds to clear
|
||||||
|
uint16_t xStart = (left < 0) ? 0 : left;
|
||||||
|
uint16_t yStart = (top < 0) ? 0 : top;
|
||||||
|
if (xStart >= driver->width || yStart >= driver->height || left + width < 0 || top + height < 0)
|
||||||
|
return; // the box is completely off the screen
|
||||||
|
uint16_t xEnd = left + width;
|
||||||
|
uint16_t yEnd = top + height;
|
||||||
|
if (xEnd > driver->width)
|
||||||
|
xEnd = driver->width;
|
||||||
|
if (yEnd > driver->height)
|
||||||
|
yEnd = driver->height;
|
||||||
|
|
||||||
|
// Clear the pixels
|
||||||
|
if (xStart == 0 && xEnd == driver->width) { // full width box is easier to clear
|
||||||
|
memset(imageBuffer + (yStart * imageBufferWidth), 0xFF, (yEnd - yStart) * imageBufferWidth);
|
||||||
|
} else {
|
||||||
|
const uint16_t byteStart = (xStart / 8) + 1;
|
||||||
|
const uint16_t byteEnd = xEnd / 8;
|
||||||
|
const uint8_t leadingByte = 0xFF >> (xStart - ((byteStart - 1) * 8));
|
||||||
|
const uint8_t trailingByte = (0xFF00 >> (xEnd - (byteEnd * 8))) & 0xFF;
|
||||||
|
for (uint16_t i = yStart * imageBufferWidth; i < yEnd * imageBufferWidth; i += imageBufferWidth) {
|
||||||
|
// Set the leading byte
|
||||||
|
imageBuffer[i + byteStart - 1] |= leadingByte;
|
||||||
|
|
||||||
|
// Set the continuous bytes
|
||||||
|
if (byteStart < byteEnd)
|
||||||
|
memset(imageBuffer + i + byteStart, 0xFF, byteEnd - byteStart);
|
||||||
|
|
||||||
|
// Set the trailing byte
|
||||||
|
if (byteEnd != imageBufferWidth)
|
||||||
|
imageBuffer[i + byteEnd] |= trailingByte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void InkHUD::Renderer::checkLocks()
|
void InkHUD::Renderer::checkLocks()
|
||||||
{
|
{
|
||||||
lockRendering = nullptr;
|
lockRendering = nullptr;
|
||||||
@@ -323,12 +397,12 @@ Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType()
|
|||||||
if (!forced) {
|
if (!forced) {
|
||||||
// User applets
|
// User applets
|
||||||
for (Applet *ua : inkhud->userApplets) {
|
for (Applet *ua : inkhud->userApplets) {
|
||||||
if (ua && ua->isForeground())
|
if (ua && ua->isForeground() && (ua->wantsToRender() || renderAll))
|
||||||
displayHealth.requestUpdateType(ua->wantsUpdateType());
|
displayHealth.requestUpdateType(ua->wantsUpdateType());
|
||||||
}
|
}
|
||||||
// System Applets
|
// System Applets
|
||||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||||
if (sa && sa->isForeground())
|
if (sa && sa->isForeground() && (sa->wantsToRender() || sa->alwaysRender || renderAll))
|
||||||
displayHealth.requestUpdateType(sa->wantsUpdateType());
|
displayHealth.requestUpdateType(sa->wantsUpdateType());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,9 +420,16 @@ void InkHUD::Renderer::renderUserApplets()
|
|||||||
|
|
||||||
// Render any user applets which are currently visible
|
// Render any user applets which are currently visible
|
||||||
for (Applet *ua : inkhud->userApplets) {
|
for (Applet *ua : inkhud->userApplets) {
|
||||||
if (ua && ua->isActive() && ua->isForeground()) {
|
if (ua && ua->isActive() && ua->isForeground() && (ua->wantsToRender() || renderAll)) {
|
||||||
|
|
||||||
|
// Clear the tile unless the applet wants to draw over its previous render
|
||||||
|
// or everything is getting re-rendered anyways
|
||||||
|
if (ua->wantsFullRender() && !renderAll)
|
||||||
|
clearTile(ua->getTile());
|
||||||
|
|
||||||
uint32_t start = millis();
|
uint32_t start = millis();
|
||||||
ua->render(); // Draw!
|
bool full = ua->wantsFullRender() || renderAll;
|
||||||
|
ua->render(full); // Draw!
|
||||||
uint32_t stop = millis();
|
uint32_t stop = millis();
|
||||||
LOG_DEBUG("%s took %dms to render", ua->name, stop - start);
|
LOG_DEBUG("%s took %dms to render", ua->name, stop - start);
|
||||||
}
|
}
|
||||||
@@ -370,6 +451,9 @@ void InkHUD::Renderer::renderSystemApplets()
|
|||||||
if (!sa->isForeground())
|
if (!sa->isForeground())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (!sa->wantsToRender() && !sa->alwaysRender && !renderAll)
|
||||||
|
continue;
|
||||||
|
|
||||||
// Skip if locked by another applet
|
// Skip if locked by another applet
|
||||||
if (lockRendering && lockRendering != sa)
|
if (lockRendering && lockRendering != sa)
|
||||||
continue;
|
continue;
|
||||||
@@ -381,8 +465,14 @@ void InkHUD::Renderer::renderSystemApplets()
|
|||||||
|
|
||||||
assert(sa->getTile());
|
assert(sa->getTile());
|
||||||
|
|
||||||
|
// Clear the tile unless the applet wants to draw over its previous render
|
||||||
|
// or everything is getting re-rendered anyways
|
||||||
|
if (sa->wantsFullRender() && !renderAll)
|
||||||
|
clearTile(sa->getTile());
|
||||||
|
|
||||||
// uint32_t start = millis();
|
// uint32_t start = millis();
|
||||||
sa->render(); // Draw!
|
bool full = sa->wantsFullRender() || renderAll;
|
||||||
|
sa->render(full); // Draw!
|
||||||
// uint32_t stop = millis();
|
// uint32_t stop = millis();
|
||||||
// LOG_DEBUG("%s took %dms to render", sa->name, stop - start);
|
// LOG_DEBUG("%s took %dms to render", sa->name, stop - start);
|
||||||
}
|
}
|
||||||
@@ -409,7 +499,10 @@ void InkHUD::Renderer::renderPlaceholders()
|
|||||||
// uint32_t start = millis();
|
// uint32_t start = millis();
|
||||||
for (Tile *t : emptyTiles) {
|
for (Tile *t : emptyTiles) {
|
||||||
t->assignApplet(placeholder);
|
t->assignApplet(placeholder);
|
||||||
placeholder->render();
|
// Clear the tile unless everything is getting re-rendered
|
||||||
|
if (!renderAll)
|
||||||
|
clearTile(t);
|
||||||
|
placeholder->render(true); // full render
|
||||||
t->assignApplet(nullptr);
|
t->assignApplet(nullptr);
|
||||||
}
|
}
|
||||||
// uint32_t stop = millis();
|
// uint32_t stop = millis();
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ class Renderer : protected concurrency::OSThread
|
|||||||
|
|
||||||
// Call these to make the image change
|
// Call these to make the image change
|
||||||
|
|
||||||
void requestUpdate(); // Update display, if a foreground applet has info it wants to show
|
void requestUpdate(bool all = false); // Update display, if a foreground applet has info it wants to show
|
||||||
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED,
|
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool all = false,
|
||||||
bool async = true); // Update display, regardless of whether any applets requested this
|
bool async = true); // Update display, regardless of whether any applets requested this
|
||||||
|
|
||||||
// Wait for an update to complete
|
// Wait for an update to complete
|
||||||
@@ -65,6 +65,7 @@ class Renderer : protected concurrency::OSThread
|
|||||||
// Steps of the rendering process
|
// Steps of the rendering process
|
||||||
|
|
||||||
void clearBuffer();
|
void clearBuffer();
|
||||||
|
void clearTile(Tile *t);
|
||||||
void checkLocks();
|
void checkLocks();
|
||||||
bool shouldUpdate();
|
bool shouldUpdate();
|
||||||
Drivers::EInk::UpdateTypes decideUpdateType();
|
Drivers::EInk::UpdateTypes decideUpdateType();
|
||||||
@@ -85,6 +86,7 @@ class Renderer : protected concurrency::OSThread
|
|||||||
|
|
||||||
bool requested = false;
|
bool requested = false;
|
||||||
bool forced = false;
|
bool forced = false;
|
||||||
|
bool renderAll = false;
|
||||||
|
|
||||||
// For convenience
|
// For convenience
|
||||||
InkHUD *inkhud = nullptr;
|
InkHUD *inkhud = nullptr;
|
||||||
|
|||||||
@@ -22,11 +22,14 @@ class SystemApplet : public Applet
|
|||||||
public:
|
public:
|
||||||
// System applets have the right to:
|
// System applets have the right to:
|
||||||
|
|
||||||
bool handleInput = false; // - respond to input from the user button
|
bool handleInput = false; // - respond to input from the user button
|
||||||
bool lockRendering = false; // - prevent other applets from being rendered during an update
|
bool handleFreeText = false; // - respond to free text input
|
||||||
bool lockRequests = false; // - prevent other applets from triggering display updates
|
bool lockRendering = false; // - prevent other applets from being rendered during an update
|
||||||
|
bool lockRequests = false; // - prevent other applets from triggering display updates
|
||||||
|
bool alwaysRender = false; // - render every time the screen is updated
|
||||||
|
|
||||||
virtual void onReboot() { onShutdown(); } // - handle reboot specially
|
virtual void onReboot() { onShutdown(); } // - handle reboot specially
|
||||||
|
virtual void onApplyingChanges() {}
|
||||||
|
|
||||||
// Other system applets may take precedence over our own system applet though
|
// Other system applets may take precedence over our own system applet though
|
||||||
// The order an applet is passed to WindowManager::addSystemApplet determines this hierarchy (added earlier = higher rank)
|
// The order an applet is passed to WindowManager::addSystemApplet determines this hierarchy (added earlier = higher rank)
|
||||||
@@ -40,4 +43,4 @@ class SystemApplet : public Applet
|
|||||||
|
|
||||||
}; // namespace NicheGraphics::InkHUD
|
}; // namespace NicheGraphics::InkHUD
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ static int32_t runtaskHighlight()
|
|||||||
LOG_DEBUG("Dismissing Highlight");
|
LOG_DEBUG("Dismissing Highlight");
|
||||||
InkHUD::Tile::highlightShown = false;
|
InkHUD::Tile::highlightShown = false;
|
||||||
InkHUD::Tile::highlightTarget = nullptr;
|
InkHUD::Tile::highlightTarget = nullptr;
|
||||||
InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting
|
InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true); // Re-render, clearing the highlighting
|
||||||
return taskHighlight->disable();
|
return taskHighlight->disable();
|
||||||
}
|
}
|
||||||
static void inittaskHighlight()
|
static void inittaskHighlight()
|
||||||
@@ -190,6 +190,18 @@ void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used in Renderer for clearing the tile
|
||||||
|
int16_t InkHUD::Tile::getLeft()
|
||||||
|
{
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used in Renderer for clearing the tile
|
||||||
|
int16_t InkHUD::Tile::getTop()
|
||||||
|
{
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
|
||||||
// Called by Applet base class, when setting applet dimensions, immediately before render
|
// Called by Applet base class, when setting applet dimensions, immediately before render
|
||||||
uint16_t InkHUD::Tile::getWidth()
|
uint16_t InkHUD::Tile::getWidth()
|
||||||
{
|
{
|
||||||
@@ -220,7 +232,7 @@ void InkHUD::Tile::requestHighlight()
|
|||||||
{
|
{
|
||||||
Tile::highlightTarget = this;
|
Tile::highlightTarget = this;
|
||||||
Tile::highlightShown = false;
|
Tile::highlightShown = false;
|
||||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST);
|
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first
|
// Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ class Tile
|
|||||||
void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout
|
void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout
|
||||||
void setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually
|
void setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually
|
||||||
void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet
|
void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet
|
||||||
|
int16_t getLeft();
|
||||||
|
int16_t getTop();
|
||||||
uint16_t getWidth();
|
uint16_t getWidth();
|
||||||
uint16_t getHeight();
|
uint16_t getHeight();
|
||||||
static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter
|
static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "./Applets/System/AlignStick/AlignStickApplet.h"
|
#include "./Applets/System/AlignStick/AlignStickApplet.h"
|
||||||
#include "./Applets/System/BatteryIcon/BatteryIconApplet.h"
|
#include "./Applets/System/BatteryIcon/BatteryIconApplet.h"
|
||||||
|
#include "./Applets/System/Keyboard/KeyboardApplet.h"
|
||||||
#include "./Applets/System/Logo/LogoApplet.h"
|
#include "./Applets/System/Logo/LogoApplet.h"
|
||||||
#include "./Applets/System/Menu/MenuApplet.h"
|
#include "./Applets/System/Menu/MenuApplet.h"
|
||||||
#include "./Applets/System/Notification/NotificationApplet.h"
|
#include "./Applets/System/Notification/NotificationApplet.h"
|
||||||
@@ -148,6 +149,28 @@ void InkHUD::WindowManager::openAlignStick()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InkHUD::WindowManager::openKeyboard()
|
||||||
|
{
|
||||||
|
KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard");
|
||||||
|
|
||||||
|
if (keyboard) {
|
||||||
|
keyboard->bringToForeground();
|
||||||
|
keyboardOpen = true;
|
||||||
|
changeLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::WindowManager::closeKeyboard()
|
||||||
|
{
|
||||||
|
KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard");
|
||||||
|
|
||||||
|
if (keyboard) {
|
||||||
|
keyboard->sendToBackground();
|
||||||
|
keyboardOpen = false;
|
||||||
|
changeLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// On the currently focussed tile: cycle to the next available user applet
|
// On the currently focussed tile: cycle to the next available user applet
|
||||||
// Applets available for this must be activated, and not already displayed on another tile
|
// Applets available for this must be activated, and not already displayed on another tile
|
||||||
void InkHUD::WindowManager::nextApplet()
|
void InkHUD::WindowManager::nextApplet()
|
||||||
@@ -272,7 +295,6 @@ void InkHUD::WindowManager::toggleBatteryIcon()
|
|||||||
batteryIcon->sendToBackground();
|
batteryIcon->sendToBackground();
|
||||||
|
|
||||||
// Force-render
|
// Force-render
|
||||||
// - redraw all applets
|
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,9 +333,25 @@ void InkHUD::WindowManager::changeLayout()
|
|||||||
menu->show(ft);
|
menu->show(ft);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resize for the on-screen keyboard
|
||||||
|
if (keyboardOpen) {
|
||||||
|
// Send all user applets to the background
|
||||||
|
// User applets currently don't handle free text input
|
||||||
|
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++)
|
||||||
|
inkhud->userApplets.at(i)->sendToBackground();
|
||||||
|
// Find the first system applet that can handle freetext and resize it
|
||||||
|
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||||
|
if (sa->handleFreeText) {
|
||||||
|
const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight();
|
||||||
|
sa->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height() - keyboardHeight - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Force-render
|
// Force-render
|
||||||
// - redraw all applets
|
// - redraw all applets
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
inkhud->forceUpdate(EInk::UpdateTypes::FAST, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform necessary reconfiguration when user activates or deactivates applets at run-time
|
// Perform necessary reconfiguration when user activates or deactivates applets at run-time
|
||||||
@@ -347,7 +385,7 @@ void InkHUD::WindowManager::changeActivatedApplets()
|
|||||||
|
|
||||||
// Force-render
|
// Force-render
|
||||||
// - redraw all applets
|
// - redraw all applets
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
inkhud->forceUpdate(EInk::UpdateTypes::FAST, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some applets may be permitted to bring themselves to foreground, to show new data
|
// Some applets may be permitted to bring themselves to foreground, to show new data
|
||||||
@@ -433,8 +471,10 @@ void InkHUD::WindowManager::createSystemApplets()
|
|||||||
addSystemApplet("Logo", new LogoApplet, new Tile);
|
addSystemApplet("Logo", new LogoApplet, new Tile);
|
||||||
addSystemApplet("Pairing", new PairingApplet, new Tile);
|
addSystemApplet("Pairing", new PairingApplet, new Tile);
|
||||||
addSystemApplet("Tips", new TipsApplet, new Tile);
|
addSystemApplet("Tips", new TipsApplet, new Tile);
|
||||||
if (settings->joystick.enabled)
|
if (settings->joystick.enabled) {
|
||||||
addSystemApplet("AlignStick", new AlignStickApplet, new Tile);
|
addSystemApplet("AlignStick", new AlignStickApplet, new Tile);
|
||||||
|
addSystemApplet("Keyboard", new KeyboardApplet, new Tile);
|
||||||
|
}
|
||||||
|
|
||||||
addSystemApplet("Menu", new MenuApplet, nullptr);
|
addSystemApplet("Menu", new MenuApplet, nullptr);
|
||||||
|
|
||||||
@@ -457,9 +497,13 @@ void InkHUD::WindowManager::placeSystemTiles()
|
|||||||
inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
||||||
inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
||||||
inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
||||||
if (settings->joystick.enabled)
|
if (settings->joystick.enabled) {
|
||||||
inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
||||||
|
const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight();
|
||||||
|
inkhud->getSystemApplet("Keyboard")
|
||||||
|
->getTile()
|
||||||
|
->setRegion(0, inkhud->height() - keyboardHeight, inkhud->width(), keyboardHeight);
|
||||||
|
}
|
||||||
inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20);
|
inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20);
|
||||||
|
|
||||||
const uint16_t batteryIconHeight = Applet::getHeaderHeight() - 2 - 2;
|
const uint16_t batteryIconHeight = Applet::getHeaderHeight() - 2 - 2;
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ class WindowManager
|
|||||||
void prevTile();
|
void prevTile();
|
||||||
void openMenu();
|
void openMenu();
|
||||||
void openAlignStick();
|
void openAlignStick();
|
||||||
|
void openKeyboard();
|
||||||
|
void closeKeyboard();
|
||||||
void nextApplet();
|
void nextApplet();
|
||||||
void prevApplet();
|
void prevApplet();
|
||||||
void rotate();
|
void rotate();
|
||||||
@@ -64,6 +66,7 @@ class WindowManager
|
|||||||
void findOrphanApplets(); // Find any applets left-behind when layout changes
|
void findOrphanApplets(); // Find any applets left-behind when layout changes
|
||||||
|
|
||||||
std::vector<Tile *> userTiles; // Tiles which can host user applets
|
std::vector<Tile *> userTiles; // Tiles which can host user applets
|
||||||
|
bool keyboardOpen = false;
|
||||||
|
|
||||||
// For convenience
|
// For convenience
|
||||||
InkHUD *inkhud = nullptr;
|
InkHUD *inkhud = nullptr;
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ class BasicExampleApplet : public Applet
|
|||||||
// You must have an onRender() method
|
// You must have an onRender() method
|
||||||
// All drawing happens here
|
// All drawing happens here
|
||||||
|
|
||||||
void onRender() override;
|
void onRender(bool full) override;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ The `onRender` method is called when the display image is redrawn. This can happ
|
|||||||
```cpp
|
```cpp
|
||||||
// All drawing happens here
|
// All drawing happens here
|
||||||
// Our basic example doesn't do anything useful. It just passively prints some text.
|
// Our basic example doesn't do anything useful. It just passively prints some text.
|
||||||
void InkHUD::BasicExampleApplet::onRender()
|
void InkHUD::BasicExampleApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
printAt(0, 0, "Hello, world!");
|
printAt(0, 0, "Hello, world!");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,20 +20,20 @@ constexpr uint8_t modifierLeftShift = 0b0001;
|
|||||||
|
|
||||||
// Num chars per key, Modulus for rotating through characters
|
// Num chars per key, Modulus for rotating through characters
|
||||||
static uint8_t HackadayCommunicatorTapMod[_TCA8418_NUM_KEYS] = {
|
static uint8_t HackadayCommunicatorTapMod[_TCA8418_NUM_KEYS] = {
|
||||||
0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||||
0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 2, 2, 2, 1, 2, 2, 0, 0, 0, 2, 1, 2, 2, 0, 1, 1, 0,
|
0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 2, 2, 2, 1, 2, 2, 0, 0, 0, 2, 1, 2, 2, 0, 1, 1, 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{},
|
static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{},
|
||||||
{},
|
{Key::FUNCTION_F1},
|
||||||
{'+'},
|
{'+'},
|
||||||
{'9'},
|
{'9'},
|
||||||
{'8'},
|
{'8'},
|
||||||
{'7'},
|
{'7'},
|
||||||
{'2'},
|
{Key::FUNCTION_F2},
|
||||||
{'3'},
|
{Key::FUNCTION_F3},
|
||||||
{'4'},
|
{Key::FUNCTION_F4},
|
||||||
{'5'},
|
{Key::FUNCTION_F5},
|
||||||
{Key::ESC},
|
{Key::ESC},
|
||||||
{'q', 'Q'},
|
{'q', 'Q'},
|
||||||
{'w', 'W'},
|
{'w', 'W'},
|
||||||
@@ -141,6 +141,7 @@ void HackadayCommunicatorKeyboard::pressed(uint8_t key)
|
|||||||
if (state == Init || state == Busy) {
|
if (state == Init || state == Busy) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
LOG_DEBUG("Key pressed: %u", key);
|
||||||
|
|
||||||
if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) {
|
if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) {
|
||||||
modifierFlag = 0;
|
modifierFlag = 0;
|
||||||
|
|||||||
@@ -1,8 +1,59 @@
|
|||||||
#include "InputBroker.h"
|
#include "InputBroker.h"
|
||||||
#include "PowerFSM.h" // needed for event trigger
|
#include "PowerFSM.h" // needed for event trigger
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "graphics/Screen.h"
|
||||||
#include "modules/ExternalNotificationModule.h"
|
#include "modules/ExternalNotificationModule.h"
|
||||||
|
|
||||||
|
#if ARCH_PORTDUINO
|
||||||
|
#include "input/LinuxInputImpl.h"
|
||||||
|
#include "input/SeesawRotary.h"
|
||||||
|
#include "platform/portduino/PortduinoGlue.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||||
|
#include "input/ExpressLRSFiveWay.h"
|
||||||
|
#include "input/RotaryEncoderImpl.h"
|
||||||
|
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||||
|
#include "input/SerialKeyboardImpl.h"
|
||||||
|
#include "input/UpDownInterruptImpl1.h"
|
||||||
|
#include "input/i2cButton.h"
|
||||||
|
#if HAS_TRACKBALL
|
||||||
|
#include "input/TrackballInterruptImpl1.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "modules/StatusLEDModule.h"
|
||||||
|
|
||||||
|
#if !MESHTASTIC_EXCLUDE_I2C
|
||||||
|
#include "input/cardKbI2cImpl.h"
|
||||||
|
#endif
|
||||||
|
#include "input/kbMatrixImpl.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if HAS_BUTTON || defined(ARCH_PORTDUINO)
|
||||||
|
#include "input/ButtonThread.h"
|
||||||
|
|
||||||
|
#if defined(BUTTON_PIN_TOUCH)
|
||||||
|
ButtonThread *TouchButtonThread = nullptr;
|
||||||
|
#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN)
|
||||||
|
static bool touchBacklightWasOn = false;
|
||||||
|
static bool touchBacklightActive = false;
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO)
|
||||||
|
ButtonThread *UserButtonThread = nullptr;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(ALT_BUTTON_PIN)
|
||||||
|
ButtonThread *BackButtonThread = nullptr;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(CANCEL_BUTTON_PIN)
|
||||||
|
ButtonThread *CancelButtonThread = nullptr;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
InputBroker *inputBroker = nullptr;
|
InputBroker *inputBroker = nullptr;
|
||||||
|
|
||||||
InputBroker::InputBroker()
|
InputBroker::InputBroker()
|
||||||
@@ -74,3 +125,262 @@ void InputBroker::pollSoonWorker(void *p)
|
|||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
void InputBroker::Init()
|
||||||
|
{
|
||||||
|
|
||||||
|
#ifdef BUTTON_PIN
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
|
||||||
|
#if ESP_ARDUINO_VERSION_MAJOR >= 3
|
||||||
|
#ifdef BUTTON_NEED_PULLUP
|
||||||
|
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP);
|
||||||
|
#else
|
||||||
|
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
|
||||||
|
#ifdef BUTTON_NEED_PULLUP
|
||||||
|
gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN));
|
||||||
|
delay(10);
|
||||||
|
#endif
|
||||||
|
#ifdef BUTTON_NEED_PULLUP2
|
||||||
|
gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2);
|
||||||
|
delay(10);
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// buttons are now inputBroker, so have to come after setupModules
|
||||||
|
#if HAS_BUTTON
|
||||||
|
int pullup_sense = 0;
|
||||||
|
#ifdef INPUT_PULLUP_SENSE
|
||||||
|
// Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did
|
||||||
|
#ifdef BUTTON_SENSE_TYPE
|
||||||
|
pullup_sense = BUTTON_SENSE_TYPE;
|
||||||
|
#else
|
||||||
|
pullup_sense = INPUT_PULLUP_SENSE;
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
#if defined(ARCH_PORTDUINO)
|
||||||
|
|
||||||
|
if (portduino_config.userButtonPin.enabled) {
|
||||||
|
|
||||||
|
LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin);
|
||||||
|
UserButtonThread = new ButtonThread("UserButton");
|
||||||
|
if (screen) {
|
||||||
|
ButtonConfig config;
|
||||||
|
config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin;
|
||||||
|
config.activeLow = true;
|
||||||
|
config.activePullup = true;
|
||||||
|
config.pullupSense = INPUT_PULLUP;
|
||||||
|
config.intRoutine = []() {
|
||||||
|
UserButtonThread->userButton.tick();
|
||||||
|
UserButtonThread->setIntervalFromNow(0);
|
||||||
|
runASAP = true;
|
||||||
|
BaseType_t higherWake = 0;
|
||||||
|
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||||
|
};
|
||||||
|
config.singlePress = INPUT_BROKER_USER_PRESS;
|
||||||
|
config.longPress = INPUT_BROKER_SELECT;
|
||||||
|
UserButtonThread->initButton(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef BUTTON_PIN_TOUCH
|
||||||
|
TouchButtonThread = new ButtonThread("BackButton");
|
||||||
|
ButtonConfig touchConfig;
|
||||||
|
touchConfig.pinNumber = BUTTON_PIN_TOUCH;
|
||||||
|
touchConfig.activeLow = true;
|
||||||
|
touchConfig.activePullup = true;
|
||||||
|
touchConfig.pullupSense = pullup_sense;
|
||||||
|
touchConfig.intRoutine = []() {
|
||||||
|
TouchButtonThread->userButton.tick();
|
||||||
|
TouchButtonThread->setIntervalFromNow(0);
|
||||||
|
runASAP = true;
|
||||||
|
BaseType_t higherWake = 0;
|
||||||
|
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||||
|
};
|
||||||
|
touchConfig.singlePress = INPUT_BROKER_NONE;
|
||||||
|
touchConfig.longPress = INPUT_BROKER_BACK;
|
||||||
|
#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN)
|
||||||
|
// On T-Echo Plus the touch pad should only drive the backlight, not UI navigation/sounds
|
||||||
|
touchConfig.longPress = INPUT_BROKER_NONE;
|
||||||
|
touchConfig.suppressLeadUpSound = true;
|
||||||
|
touchConfig.onPress = []() {
|
||||||
|
touchBacklightWasOn = uiconfig.screen_brightness == 1;
|
||||||
|
if (!touchBacklightWasOn) {
|
||||||
|
digitalWrite(PIN_EINK_EN, HIGH);
|
||||||
|
}
|
||||||
|
touchBacklightActive = true;
|
||||||
|
};
|
||||||
|
touchConfig.onRelease = []() {
|
||||||
|
if (touchBacklightActive && !touchBacklightWasOn) {
|
||||||
|
digitalWrite(PIN_EINK_EN, LOW);
|
||||||
|
}
|
||||||
|
touchBacklightActive = false;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
TouchButtonThread->initButton(touchConfig);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(CANCEL_BUTTON_PIN)
|
||||||
|
// Buttons. Moved here cause we need NodeDB to be initialized
|
||||||
|
CancelButtonThread = new ButtonThread("CancelButton");
|
||||||
|
ButtonConfig cancelConfig;
|
||||||
|
cancelConfig.pinNumber = CANCEL_BUTTON_PIN;
|
||||||
|
cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW;
|
||||||
|
cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP;
|
||||||
|
cancelConfig.pullupSense = pullup_sense;
|
||||||
|
cancelConfig.intRoutine = []() {
|
||||||
|
CancelButtonThread->userButton.tick();
|
||||||
|
CancelButtonThread->setIntervalFromNow(0);
|
||||||
|
runASAP = true;
|
||||||
|
BaseType_t higherWake = 0;
|
||||||
|
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||||
|
};
|
||||||
|
cancelConfig.singlePress = INPUT_BROKER_CANCEL;
|
||||||
|
cancelConfig.longPress = INPUT_BROKER_SHUTDOWN;
|
||||||
|
cancelConfig.longPressTime = 4000;
|
||||||
|
CancelButtonThread->initButton(cancelConfig);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(ALT_BUTTON_PIN)
|
||||||
|
// Buttons. Moved here cause we need NodeDB to be initialized
|
||||||
|
BackButtonThread = new ButtonThread("BackButton");
|
||||||
|
ButtonConfig backConfig;
|
||||||
|
backConfig.pinNumber = ALT_BUTTON_PIN;
|
||||||
|
backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW;
|
||||||
|
backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP;
|
||||||
|
backConfig.pullupSense = pullup_sense;
|
||||||
|
backConfig.intRoutine = []() {
|
||||||
|
BackButtonThread->userButton.tick();
|
||||||
|
BackButtonThread->setIntervalFromNow(0);
|
||||||
|
runASAP = true;
|
||||||
|
BaseType_t higherWake = 0;
|
||||||
|
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||||
|
};
|
||||||
|
backConfig.singlePress = INPUT_BROKER_ALT_PRESS;
|
||||||
|
backConfig.longPress = INPUT_BROKER_ALT_LONG;
|
||||||
|
backConfig.longPressTime = 500;
|
||||||
|
BackButtonThread->initButton(backConfig);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(BUTTON_PIN)
|
||||||
|
#if defined(USERPREFS_BUTTON_PIN)
|
||||||
|
int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN;
|
||||||
|
#else
|
||||||
|
int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN;
|
||||||
|
#endif
|
||||||
|
#ifndef BUTTON_ACTIVE_LOW
|
||||||
|
#define BUTTON_ACTIVE_LOW true
|
||||||
|
#endif
|
||||||
|
#ifndef BUTTON_ACTIVE_PULLUP
|
||||||
|
#define BUTTON_ACTIVE_PULLUP true
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Buttons. Moved here cause we need NodeDB to be initialized
|
||||||
|
// If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP
|
||||||
|
UserButtonThread = new ButtonThread("UserButton");
|
||||||
|
if (screen) {
|
||||||
|
ButtonConfig userConfig;
|
||||||
|
userConfig.pinNumber = (uint8_t)_pinNum;
|
||||||
|
userConfig.activeLow = BUTTON_ACTIVE_LOW;
|
||||||
|
userConfig.activePullup = BUTTON_ACTIVE_PULLUP;
|
||||||
|
userConfig.pullupSense = pullup_sense;
|
||||||
|
userConfig.intRoutine = []() {
|
||||||
|
UserButtonThread->userButton.tick();
|
||||||
|
UserButtonThread->setIntervalFromNow(0);
|
||||||
|
runASAP = true;
|
||||||
|
BaseType_t higherWake = 0;
|
||||||
|
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||||
|
};
|
||||||
|
userConfig.singlePress = INPUT_BROKER_USER_PRESS;
|
||||||
|
userConfig.longPress = INPUT_BROKER_SELECT;
|
||||||
|
userConfig.longPressTime = 500;
|
||||||
|
userConfig.longLongPress = INPUT_BROKER_SHUTDOWN;
|
||||||
|
UserButtonThread->initButton(userConfig);
|
||||||
|
} else {
|
||||||
|
ButtonConfig userConfigNoScreen;
|
||||||
|
userConfigNoScreen.pinNumber = (uint8_t)_pinNum;
|
||||||
|
userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW;
|
||||||
|
userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP;
|
||||||
|
userConfigNoScreen.pullupSense = pullup_sense;
|
||||||
|
userConfigNoScreen.intRoutine = []() {
|
||||||
|
UserButtonThread->userButton.tick();
|
||||||
|
UserButtonThread->setIntervalFromNow(0);
|
||||||
|
runASAP = true;
|
||||||
|
BaseType_t higherWake = 0;
|
||||||
|
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||||
|
};
|
||||||
|
userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS;
|
||||||
|
userConfigNoScreen.longPress = INPUT_BROKER_NONE;
|
||||||
|
userConfigNoScreen.longPressTime = 500;
|
||||||
|
userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN;
|
||||||
|
userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING;
|
||||||
|
userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE;
|
||||||
|
UserButtonThread->initButton(userConfigNoScreen);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||||
|
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||||
|
#if defined(T_LORA_PAGER)
|
||||||
|
// use a special FSM based rotary encoder version for T-LoRa Pager
|
||||||
|
rotaryEncoderImpl = new RotaryEncoderImpl();
|
||||||
|
if (!rotaryEncoderImpl->init()) {
|
||||||
|
delete rotaryEncoderImpl;
|
||||||
|
rotaryEncoderImpl = nullptr;
|
||||||
|
}
|
||||||
|
#elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2)
|
||||||
|
upDownInterruptImpl1 = new UpDownInterruptImpl1();
|
||||||
|
if (!upDownInterruptImpl1->init()) {
|
||||||
|
delete upDownInterruptImpl1;
|
||||||
|
upDownInterruptImpl1 = nullptr;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
|
||||||
|
if (!rotaryEncoderInterruptImpl1->init()) {
|
||||||
|
delete rotaryEncoderInterruptImpl1;
|
||||||
|
rotaryEncoderInterruptImpl1 = nullptr;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
cardKbI2cImpl = new CardKbI2cImpl();
|
||||||
|
cardKbI2cImpl->init();
|
||||||
|
#if defined(M5STACK_UNITC6L)
|
||||||
|
i2cButton = new i2cButtonThread("i2cButtonThread");
|
||||||
|
#endif
|
||||||
|
#ifdef INPUTBROKER_MATRIX_TYPE
|
||||||
|
kbMatrixImpl = new KbMatrixImpl();
|
||||||
|
kbMatrixImpl->init();
|
||||||
|
#endif // INPUTBROKER_MATRIX_TYPE
|
||||||
|
#ifdef INPUTBROKER_SERIAL_TYPE
|
||||||
|
aSerialKeyboardImpl = new SerialKeyboardImpl();
|
||||||
|
aSerialKeyboardImpl->init();
|
||||||
|
#endif // INPUTBROKER_MATRIX_TYPE
|
||||||
|
}
|
||||||
|
#endif // HAS_BUTTON
|
||||||
|
#if ARCH_PORTDUINO
|
||||||
|
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") {
|
||||||
|
seesawRotary = new SeesawRotary("SeesawRotary");
|
||||||
|
if (!seesawRotary->init()) {
|
||||||
|
delete seesawRotary;
|
||||||
|
seesawRotary = nullptr;
|
||||||
|
}
|
||||||
|
aLinuxInputImpl = new LinuxInputImpl();
|
||||||
|
aLinuxInputImpl->init();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL
|
||||||
|
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||||
|
trackballInterruptImpl1 = new TrackballInterruptImpl1();
|
||||||
|
trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE
|
||||||
|
expressLRSFiveWayInput = new ExpressLRSFiveWay();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "Observer.h"
|
#include "Observer.h"
|
||||||
|
#include "concurrency/OSThread.h"
|
||||||
#include "freertosinc.h"
|
#include "freertosinc.h"
|
||||||
|
|
||||||
#ifdef InputBrokerDebug
|
#ifdef InputBrokerDebug
|
||||||
@@ -27,6 +28,11 @@ enum input_broker_event {
|
|||||||
INPUT_BROKER_SHUTDOWN = 0x9b,
|
INPUT_BROKER_SHUTDOWN = 0x9b,
|
||||||
INPUT_BROKER_GPS_TOGGLE = 0x9e,
|
INPUT_BROKER_GPS_TOGGLE = 0x9e,
|
||||||
INPUT_BROKER_SEND_PING = 0xaf,
|
INPUT_BROKER_SEND_PING = 0xaf,
|
||||||
|
INPUT_BROKER_FN_F1 = 0xf1,
|
||||||
|
INPUT_BROKER_FN_F2 = 0xf2,
|
||||||
|
INPUT_BROKER_FN_F3 = 0xf3,
|
||||||
|
INPUT_BROKER_FN_F4 = 0xf4,
|
||||||
|
INPUT_BROKER_FN_F5 = 0xf5,
|
||||||
INPUT_BROKER_MATRIXKEY = 0xFE,
|
INPUT_BROKER_MATRIXKEY = 0xFE,
|
||||||
INPUT_BROKER_ANYKEY = 0xff
|
INPUT_BROKER_ANYKEY = 0xff
|
||||||
|
|
||||||
@@ -71,6 +77,7 @@ class InputBroker : public Observable<const InputEvent *>
|
|||||||
void queueInputEvent(const InputEvent *event);
|
void queueInputEvent(const InputEvent *event);
|
||||||
void processInputEventQueue();
|
void processInputEventQueue();
|
||||||
#endif
|
#endif
|
||||||
|
void Init();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int handleInputEvent(const InputEvent *event);
|
int handleInputEvent(const InputEvent *event);
|
||||||
@@ -84,4 +91,5 @@ class InputBroker : public Observable<const InputEvent *>
|
|||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
extern InputBroker *inputBroker;
|
extern InputBroker *inputBroker;
|
||||||
|
extern bool runASAP;
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
SerialKeyboard *globalSerialKeyboard = nullptr;
|
SerialKeyboard *globalSerialKeyboard = nullptr;
|
||||||
|
|
||||||
#ifdef INPUTBROKER_SERIAL_TYPE
|
#ifdef INPUTBROKER_SERIAL_TYPE
|
||||||
#define CANNED_MESSAGE_MODULE_ENABLE 1 // in case it's not set in the variant file
|
|
||||||
|
|
||||||
#if INPUTBROKER_SERIAL_TYPE == 1 // It's a Chatter
|
#if INPUTBROKER_SERIAL_TYPE == 1 // It's a Chatter
|
||||||
// 3 SHIFT level (lower case, upper case, numbers), up to 4 repeated presses, button number
|
// 3 SHIFT level (lower case, upper case, numbers), up to 4 repeated presses, button number
|
||||||
|
|||||||
@@ -26,7 +26,12 @@ class TCA8418KeyboardBase
|
|||||||
GPS_TOGGLE = 0x9E,
|
GPS_TOGGLE = 0x9E,
|
||||||
MUTE_TOGGLE = 0xAC,
|
MUTE_TOGGLE = 0xAC,
|
||||||
SEND_PING = 0xAF,
|
SEND_PING = 0xAF,
|
||||||
BL_TOGGLE = 0xAB
|
BL_TOGGLE = 0xAB,
|
||||||
|
FUNCTION_F1 = 0xF1,
|
||||||
|
FUNCTION_F2 = 0xF2,
|
||||||
|
FUNCTION_F3 = 0xF3,
|
||||||
|
FUNCTION_F4 = 0xF4,
|
||||||
|
FUNCTION_F5 = 0xF5
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len);
|
typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "TrackballInterruptBase.h"
|
#include "TrackballInterruptBase.h"
|
||||||
|
#include "Throttle.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
|
||||||
extern bool osk_found;
|
extern bool osk_found;
|
||||||
|
|
||||||
TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {}
|
TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {}
|
||||||
@@ -55,17 +57,27 @@ int32_t TrackballInterruptBase::runOnce()
|
|||||||
{
|
{
|
||||||
InputEvent e = {};
|
InputEvent e = {};
|
||||||
e.inputEvent = INPUT_BROKER_NONE;
|
e.inputEvent = INPUT_BROKER_NONE;
|
||||||
|
#if TB_THRESHOLD
|
||||||
|
if (lastInterruptTime && !Throttle::isWithinTimespanMs(lastInterruptTime, 1000)) {
|
||||||
|
left_counter = 0;
|
||||||
|
right_counter = 0;
|
||||||
|
up_counter = 0;
|
||||||
|
down_counter = 0;
|
||||||
|
lastInterruptTime = 0;
|
||||||
|
}
|
||||||
|
#ifdef INPUT_DEBUG
|
||||||
|
if (left_counter > 0 || right_counter > 0 || up_counter > 0 || down_counter > 0) {
|
||||||
|
LOG_DEBUG("L %u R %u U %u D %u, time %u", left_counter, right_counter, up_counter, down_counter, millis());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
// Handle long press detection for press button
|
// Handle long press detection for press button
|
||||||
if (pressDetected && pressStartTime > 0) {
|
if (pressDetected && pressStartTime > 0) {
|
||||||
uint32_t pressDuration = millis() - pressStartTime;
|
uint32_t pressDuration = millis() - pressStartTime;
|
||||||
bool buttonStillPressed = false;
|
bool buttonStillPressed = false;
|
||||||
|
|
||||||
#if defined(T_DECK)
|
|
||||||
buttonStillPressed = (this->action == TB_ACTION_PRESSED);
|
|
||||||
#else
|
|
||||||
buttonStillPressed = !digitalRead(_pinPress);
|
buttonStillPressed = !digitalRead(_pinPress);
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!buttonStillPressed) {
|
if (!buttonStillPressed) {
|
||||||
// Button released
|
// Button released
|
||||||
@@ -134,23 +146,31 @@ int32_t TrackballInterruptBase::runOnce()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball
|
#if TB_THRESHOLD
|
||||||
if (this->action == TB_ACTION_PRESSED && !pressDetected) {
|
if (this->action == TB_ACTION_PRESSED && (!pressDetected || pressStartTime == 0)) {
|
||||||
// Start long press detection
|
// Start long press detection
|
||||||
pressDetected = true;
|
pressDetected = true;
|
||||||
pressStartTime = millis();
|
pressStartTime = millis();
|
||||||
// Don't send event yet, wait to see if it's a long press
|
// Don't send event yet, wait to see if it's a long press
|
||||||
} else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) {
|
} else if (up_counter >= TB_THRESHOLD) {
|
||||||
// LOG_DEBUG("Trackball event UP");
|
#ifdef INPUT_DEBUG
|
||||||
|
LOG_DEBUG("Trackball event UP %u", millis());
|
||||||
|
#endif
|
||||||
e.inputEvent = this->_eventUp;
|
e.inputEvent = this->_eventUp;
|
||||||
} else if (this->action == TB_ACTION_DOWN && lastEvent == TB_ACTION_DOWN) {
|
} else if (down_counter >= TB_THRESHOLD) {
|
||||||
// LOG_DEBUG("Trackball event DOWN");
|
#ifdef INPUT_DEBUG
|
||||||
|
LOG_DEBUG("Trackball event DOWN %u", millis());
|
||||||
|
#endif
|
||||||
e.inputEvent = this->_eventDown;
|
e.inputEvent = this->_eventDown;
|
||||||
} else if (this->action == TB_ACTION_LEFT && lastEvent == TB_ACTION_LEFT) {
|
} else if (left_counter >= TB_THRESHOLD) {
|
||||||
// LOG_DEBUG("Trackball event LEFT");
|
#ifdef INPUT_DEBUG
|
||||||
|
LOG_DEBUG("Trackball event LEFT %u", millis());
|
||||||
|
#endif
|
||||||
e.inputEvent = this->_eventLeft;
|
e.inputEvent = this->_eventLeft;
|
||||||
} else if (this->action == TB_ACTION_RIGHT && lastEvent == TB_ACTION_RIGHT) {
|
} else if (right_counter >= TB_THRESHOLD) {
|
||||||
// LOG_DEBUG("Trackball event RIGHT");
|
#ifdef INPUT_DEBUG
|
||||||
|
LOG_DEBUG("Trackball event RIGHT %u", millis());
|
||||||
|
#endif
|
||||||
e.inputEvent = this->_eventRight;
|
e.inputEvent = this->_eventRight;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
@@ -183,6 +203,12 @@ int32_t TrackballInterruptBase::runOnce()
|
|||||||
e.source = this->_originName;
|
e.source = this->_originName;
|
||||||
e.kbchar = 0x00;
|
e.kbchar = 0x00;
|
||||||
this->notifyObservers(&e);
|
this->notifyObservers(&e);
|
||||||
|
#if TB_THRESHOLD
|
||||||
|
left_counter = 0;
|
||||||
|
right_counter = 0;
|
||||||
|
up_counter = 0;
|
||||||
|
down_counter = 0;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only update lastEvent for non-press actions or completed press actions
|
// Only update lastEvent for non-press actions or completed press actions
|
||||||
@@ -198,25 +224,49 @@ int32_t TrackballInterruptBase::runOnce()
|
|||||||
|
|
||||||
void TrackballInterruptBase::intPressHandler()
|
void TrackballInterruptBase::intPressHandler()
|
||||||
{
|
{
|
||||||
this->action = TB_ACTION_PRESSED;
|
if (!Throttle::isWithinTimespanMs(lastInterruptTime, 10))
|
||||||
|
this->action = TB_ACTION_PRESSED;
|
||||||
|
lastInterruptTime = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TrackballInterruptBase::intDownHandler()
|
void TrackballInterruptBase::intDownHandler()
|
||||||
{
|
{
|
||||||
this->action = TB_ACTION_DOWN;
|
if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10))
|
||||||
|
this->action = TB_ACTION_DOWN;
|
||||||
|
lastInterruptTime = millis();
|
||||||
|
|
||||||
|
#if TB_THRESHOLD
|
||||||
|
down_counter++;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void TrackballInterruptBase::intUpHandler()
|
void TrackballInterruptBase::intUpHandler()
|
||||||
{
|
{
|
||||||
this->action = TB_ACTION_UP;
|
if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10))
|
||||||
|
this->action = TB_ACTION_UP;
|
||||||
|
lastInterruptTime = millis();
|
||||||
|
|
||||||
|
#if TB_THRESHOLD
|
||||||
|
up_counter++;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void TrackballInterruptBase::intLeftHandler()
|
void TrackballInterruptBase::intLeftHandler()
|
||||||
{
|
{
|
||||||
this->action = TB_ACTION_LEFT;
|
if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10))
|
||||||
|
this->action = TB_ACTION_LEFT;
|
||||||
|
lastInterruptTime = millis();
|
||||||
|
#if TB_THRESHOLD
|
||||||
|
left_counter++;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void TrackballInterruptBase::intRightHandler()
|
void TrackballInterruptBase::intRightHandler()
|
||||||
{
|
{
|
||||||
this->action = TB_ACTION_RIGHT;
|
if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10))
|
||||||
|
this->action = TB_ACTION_RIGHT;
|
||||||
|
lastInterruptTime = millis();
|
||||||
|
#if TB_THRESHOLD
|
||||||
|
right_counter++;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef TB_THRESHOLD
|
||||||
|
#define TB_THRESHOLD 0
|
||||||
|
#endif
|
||||||
|
|
||||||
class TrackballInterruptBase : public Observable<const InputEvent *>, public concurrency::OSThread
|
class TrackballInterruptBase : public Observable<const InputEvent *>, public concurrency::OSThread
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -25,8 +29,6 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
|
|||||||
void intUpHandler();
|
void intUpHandler();
|
||||||
void intLeftHandler();
|
void intLeftHandler();
|
||||||
void intRightHandler();
|
void intRightHandler();
|
||||||
uint32_t lastTime = 0;
|
|
||||||
|
|
||||||
virtual int32_t runOnce() override;
|
virtual int32_t runOnce() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -67,4 +69,12 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
|
|||||||
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
|
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
|
||||||
const char *_originName;
|
const char *_originName;
|
||||||
TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE;
|
TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE;
|
||||||
|
volatile uint32_t lastInterruptTime = 0;
|
||||||
|
|
||||||
|
#if TB_THRESHOLD
|
||||||
|
volatile uint8_t left_counter = 0;
|
||||||
|
volatile uint8_t right_counter = 0;
|
||||||
|
volatile uint8_t up_counter = 0;
|
||||||
|
volatile uint8_t down_counter = 0;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,41 +24,26 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe
|
|||||||
|
|
||||||
void TrackballInterruptImpl1::handleIntDown()
|
void TrackballInterruptImpl1::handleIntDown()
|
||||||
{
|
{
|
||||||
if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) {
|
trackballInterruptImpl1->intDownHandler();
|
||||||
trackballInterruptImpl1->lastTime = millis();
|
trackballInterruptImpl1->setIntervalFromNow(20);
|
||||||
trackballInterruptImpl1->intDownHandler();
|
|
||||||
trackballInterruptImpl1->setIntervalFromNow(20);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
void TrackballInterruptImpl1::handleIntUp()
|
void TrackballInterruptImpl1::handleIntUp()
|
||||||
{
|
{
|
||||||
if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) {
|
trackballInterruptImpl1->intUpHandler();
|
||||||
trackballInterruptImpl1->lastTime = millis();
|
trackballInterruptImpl1->setIntervalFromNow(20);
|
||||||
trackballInterruptImpl1->intUpHandler();
|
|
||||||
trackballInterruptImpl1->setIntervalFromNow(20);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
void TrackballInterruptImpl1::handleIntLeft()
|
void TrackballInterruptImpl1::handleIntLeft()
|
||||||
{
|
{
|
||||||
if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) {
|
trackballInterruptImpl1->intLeftHandler();
|
||||||
trackballInterruptImpl1->lastTime = millis();
|
trackballInterruptImpl1->setIntervalFromNow(20);
|
||||||
trackballInterruptImpl1->intLeftHandler();
|
|
||||||
trackballInterruptImpl1->setIntervalFromNow(20);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
void TrackballInterruptImpl1::handleIntRight()
|
void TrackballInterruptImpl1::handleIntRight()
|
||||||
{
|
{
|
||||||
if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) {
|
trackballInterruptImpl1->intRightHandler();
|
||||||
trackballInterruptImpl1->lastTime = millis();
|
trackballInterruptImpl1->setIntervalFromNow(20);
|
||||||
trackballInterruptImpl1->intRightHandler();
|
|
||||||
trackballInterruptImpl1->setIntervalFromNow(20);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
void TrackballInterruptImpl1::handleIntPressed()
|
void TrackballInterruptImpl1::handleIntPressed()
|
||||||
{
|
{
|
||||||
if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) {
|
trackballInterruptImpl1->intPressHandler();
|
||||||
trackballInterruptImpl1->lastTime = millis();
|
trackballInterruptImpl1->setIntervalFromNow(20);
|
||||||
trackballInterruptImpl1->intPressHandler();
|
|
||||||
trackballInterruptImpl1->setIntervalFromNow(20);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -321,6 +321,26 @@ int32_t KbI2cBase::runOnce()
|
|||||||
e.inputEvent = INPUT_BROKER_ANYKEY;
|
e.inputEvent = INPUT_BROKER_ANYKEY;
|
||||||
e.kbchar = INPUT_BROKER_MSG_TAB;
|
e.kbchar = INPUT_BROKER_MSG_TAB;
|
||||||
break;
|
break;
|
||||||
|
case TCA8418KeyboardBase::FUNCTION_F1:
|
||||||
|
e.inputEvent = INPUT_BROKER_FN_F1;
|
||||||
|
e.kbchar = 0x00;
|
||||||
|
break;
|
||||||
|
case TCA8418KeyboardBase::FUNCTION_F2:
|
||||||
|
e.inputEvent = INPUT_BROKER_FN_F2;
|
||||||
|
e.kbchar = 0x00;
|
||||||
|
break;
|
||||||
|
case TCA8418KeyboardBase::FUNCTION_F3:
|
||||||
|
e.inputEvent = INPUT_BROKER_FN_F3;
|
||||||
|
e.kbchar = 0x00;
|
||||||
|
break;
|
||||||
|
case TCA8418KeyboardBase::FUNCTION_F4:
|
||||||
|
e.inputEvent = INPUT_BROKER_FN_F4;
|
||||||
|
e.kbchar = 0x00;
|
||||||
|
break;
|
||||||
|
case TCA8418KeyboardBase::FUNCTION_F5:
|
||||||
|
e.inputEvent = INPUT_BROKER_FN_F5;
|
||||||
|
e.kbchar = 0x00;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
if (nextEvent > 127) {
|
if (nextEvent > 127) {
|
||||||
e.inputEvent = INPUT_BROKER_NONE;
|
e.inputEvent = INPUT_BROKER_NONE;
|
||||||
|
|||||||
705
src/main.cpp
705
src/main.cpp
@@ -10,6 +10,7 @@
|
|||||||
#include "ReliableRouter.h"
|
#include "ReliableRouter.h"
|
||||||
#include "airtime.h"
|
#include "airtime.h"
|
||||||
#include "buzz.h"
|
#include "buzz.h"
|
||||||
|
#include "power/PowerHAL.h"
|
||||||
|
|
||||||
#include "FSCommon.h"
|
#include "FSCommon.h"
|
||||||
#include "Led.h"
|
#include "Led.h"
|
||||||
@@ -42,10 +43,6 @@
|
|||||||
#include "MessageStore.h"
|
#include "MessageStore.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef ELECROW_ThinkNode_M5
|
|
||||||
PCA9557 io(0x18, &Wire);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
#include "freertosinc.h"
|
#include "freertosinc.h"
|
||||||
#if !MESHTASTIC_EXCLUDE_WEBSERVER
|
#if !MESHTASTIC_EXCLUDE_WEBSERVER
|
||||||
@@ -76,59 +73,52 @@ NRF52Bluetooth *nrf52Bluetooth = nullptr;
|
|||||||
#include "mqtt/MQTT.h"
|
#include "mqtt/MQTT.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "LLCC68Interface.h"
|
|
||||||
#include "LR1110Interface.h"
|
|
||||||
#include "LR1120Interface.h"
|
|
||||||
#include "LR1121Interface.h"
|
|
||||||
#include "RF95Interface.h"
|
|
||||||
#include "SX1262Interface.h"
|
|
||||||
#include "SX1268Interface.h"
|
|
||||||
#include "SX1280Interface.h"
|
|
||||||
#include "detect/LoRaRadioType.h"
|
|
||||||
|
|
||||||
#ifdef ARCH_STM32WL
|
|
||||||
#include "STM32WLE5JCInterface.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(ARCH_PORTDUINO)
|
|
||||||
#include "platform/portduino/SimRadio.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
#include "linux/LinuxHardwareI2C.h"
|
#include "linux/LinuxHardwareI2C.h"
|
||||||
#include "mesh/raspihttp/PiWebServer.h"
|
#include "mesh/raspihttp/PiWebServer.h"
|
||||||
#include "platform/portduino/PortduinoGlue.h"
|
#include "platform/portduino/PortduinoGlue.h"
|
||||||
#include "platform/portduino/USBHal.h"
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if HAS_BUTTON || defined(ARCH_PORTDUINO)
|
#ifdef ARCH_ESP32
|
||||||
#include "input/ButtonThread.h"
|
#ifdef DEBUG_PARTITION_TABLE
|
||||||
|
#include "esp_partition.h"
|
||||||
|
|
||||||
#if defined(BUTTON_PIN_TOUCH)
|
void printPartitionTable()
|
||||||
ButtonThread *TouchButtonThread = nullptr;
|
{
|
||||||
#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN)
|
printf("\n--- Partition Table ---\n");
|
||||||
static bool touchBacklightWasOn = false;
|
// Print Column Headers
|
||||||
static bool touchBacklightActive = false;
|
printf("| %-16s | %-4s | %-7s | %-10s | %-10s |\n", "Label", "Type", "Subtype", "Offset", "Size");
|
||||||
#endif
|
printf("|------------------|------|---------|------------|------------|\n");
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO)
|
// Create an iterator to find ALL partitions (Type ANY, Subtype ANY)
|
||||||
ButtonThread *UserButtonThread = nullptr;
|
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(ALT_BUTTON_PIN)
|
// Loop through the iterator
|
||||||
ButtonThread *BackButtonThread = nullptr;
|
if (it != NULL) {
|
||||||
#endif
|
do {
|
||||||
|
const esp_partition_t *part = esp_partition_get(it);
|
||||||
|
|
||||||
#if defined(CANCEL_BUTTON_PIN)
|
// Print details: Label, Type (Hex), Subtype (Hex), Offset (Hex), Size (Hex)
|
||||||
ButtonThread *CancelButtonThread = nullptr;
|
printf("| %-16s | 0x%02x | 0x%02x | 0x%08x | 0x%08x |\n", part->label, part->type, part->subtype, part->address,
|
||||||
#endif
|
part->size);
|
||||||
|
|
||||||
#endif
|
// Move to next partition
|
||||||
|
it = esp_partition_next(it);
|
||||||
|
} while (it != NULL);
|
||||||
|
|
||||||
|
// Release the iterator memory
|
||||||
|
esp_partition_iterator_release(it);
|
||||||
|
} else {
|
||||||
|
printf("No partitions found.\n");
|
||||||
|
}
|
||||||
|
printf("-----------------------\n");
|
||||||
|
}
|
||||||
|
#endif // DEBUG_PARTITION_TABLE
|
||||||
|
#endif // ARCH_ESP32
|
||||||
|
|
||||||
#include "AmbientLightingThread.h"
|
#include "AmbientLightingThread.h"
|
||||||
#include "PowerFSMThread.h"
|
#include "PowerFSMThread.h"
|
||||||
@@ -216,9 +206,6 @@ ScanI2C::DeviceAddress aqi_found = ScanI2C::ADDRESS_NONE;
|
|||||||
Adafruit_DRV2605 drv;
|
Adafruit_DRV2605 drv;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Global LoRa radio type
|
|
||||||
LoRaRadioType radioType = NO_RADIO;
|
|
||||||
|
|
||||||
bool isVibrating = false;
|
bool isVibrating = false;
|
||||||
|
|
||||||
bool eink_found = true;
|
bool eink_found = true;
|
||||||
@@ -255,6 +242,7 @@ const char *getDeviceName()
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO remove from main.cpp
|
||||||
static int32_t ledBlinker()
|
static int32_t ledBlinker()
|
||||||
{
|
{
|
||||||
// Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if
|
// Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if
|
||||||
@@ -295,6 +283,46 @@ __attribute__((weak, noinline)) bool loopCanSleep()
|
|||||||
void lateInitVariant() __attribute__((weak));
|
void lateInitVariant() __attribute__((weak));
|
||||||
void lateInitVariant() {}
|
void lateInitVariant() {}
|
||||||
|
|
||||||
|
void earlyInitVariant() __attribute__((weak));
|
||||||
|
void earlyInitVariant() {}
|
||||||
|
|
||||||
|
// NRF52 (and probably other platforms) can report when system is in power failure mode
|
||||||
|
// (eg. too low battery voltage) and operating it is unsafe (data corruption, bootloops, etc).
|
||||||
|
// For example NRF52 will prevent any flash writes in that case automatically
|
||||||
|
// (but it causes issues we need to handle).
|
||||||
|
// This detection is independent from whatever ADC or dividers used in Meshtastic
|
||||||
|
// boards and is internal to chip.
|
||||||
|
|
||||||
|
// we use powerHAL layer to get this info and delay booting until power level is safe
|
||||||
|
|
||||||
|
// wait until power level is safe to continue booting (to avoid bootloops)
|
||||||
|
// blink user led in 3 flashes sequence to indicate what is happening
|
||||||
|
void waitUntilPowerLevelSafe()
|
||||||
|
{
|
||||||
|
|
||||||
|
#ifdef LED_PIN
|
||||||
|
pinMode(LED_PIN, OUTPUT);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
while (powerHAL_isPowerLevelSafe() == false) {
|
||||||
|
|
||||||
|
#ifdef LED_PIN
|
||||||
|
|
||||||
|
// 3x: blink for 300 ms, pause for 300 ms
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
digitalWrite(LED_PIN, LED_STATE_ON);
|
||||||
|
delay(300);
|
||||||
|
digitalWrite(LED_PIN, LED_STATE_OFF);
|
||||||
|
delay(300);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// sleep for 2s
|
||||||
|
delay(2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Print info as a structured log message (for automated log processing)
|
* Print info as a structured log message (for automated log processing)
|
||||||
*/
|
*/
|
||||||
@@ -305,34 +333,30 @@ void printInfo()
|
|||||||
#ifndef PIO_UNIT_TESTING
|
#ifndef PIO_UNIT_TESTING
|
||||||
void setup()
|
void setup()
|
||||||
{
|
{
|
||||||
#if defined(R1_NEO)
|
|
||||||
pinMode(DCDC_EN_HOLD, OUTPUT);
|
// initialize power HAL layer as early as possible
|
||||||
digitalWrite(DCDC_EN_HOLD, HIGH);
|
powerHAL_init();
|
||||||
pinMode(NRF_ON, OUTPUT);
|
|
||||||
digitalWrite(NRF_ON, HIGH);
|
// prevent booting if device is in power failure mode
|
||||||
#endif
|
// boot sequence will follow when battery level raises to safe mode
|
||||||
|
waitUntilPowerLevelSafe();
|
||||||
|
|
||||||
|
// Defined in variant.cpp for early init code
|
||||||
|
earlyInitVariant();
|
||||||
|
|
||||||
#if defined(PIN_POWER_EN)
|
#if defined(PIN_POWER_EN)
|
||||||
pinMode(PIN_POWER_EN, OUTPUT);
|
pinMode(PIN_POWER_EN, OUTPUT);
|
||||||
digitalWrite(PIN_POWER_EN, HIGH);
|
digitalWrite(PIN_POWER_EN, HIGH);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(ELECROW_ThinkNode_M5)
|
|
||||||
Wire.begin(48, 47);
|
|
||||||
io.pinMode(PCA_PIN_EINK_EN, OUTPUT);
|
|
||||||
io.pinMode(PCA_PIN_POWER_EN, OUTPUT);
|
|
||||||
io.digitalWrite(PCA_PIN_POWER_EN, HIGH);
|
|
||||||
// io.pinMode(C2_PIN, OUTPUT);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef LED_POWER
|
#ifdef LED_POWER
|
||||||
pinMode(LED_POWER, OUTPUT);
|
pinMode(LED_POWER, OUTPUT);
|
||||||
digitalWrite(LED_POWER, LED_STATE_ON);
|
digitalWrite(LED_POWER, LED_STATE_ON);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USER_LED
|
#ifdef LED_NOTIFICATION
|
||||||
pinMode(USER_LED, OUTPUT);
|
pinMode(LED_NOTIFICATION, OUTPUT);
|
||||||
digitalWrite(USER_LED, HIGH ^ LED_STATE_ON);
|
digitalWrite(LED_NOTIFICATION, HIGH ^ LED_STATE_ON);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef WIFI_LED
|
#ifdef WIFI_LED
|
||||||
@@ -349,68 +373,7 @@ void setup()
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(T_DECK)
|
|
||||||
// GPIO10 manages all peripheral power supplies
|
|
||||||
// Turn on peripheral power immediately after MUC starts.
|
|
||||||
// If some boards are turned on late, ESP32 will reset due to low voltage.
|
|
||||||
// ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) ,
|
|
||||||
// TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder)
|
|
||||||
pinMode(KB_POWERON, OUTPUT);
|
|
||||||
digitalWrite(KB_POWERON, HIGH);
|
|
||||||
// T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus
|
|
||||||
// We need to initialize all CS pins in advance otherwise there will be SPI communication issues
|
|
||||||
// e.g. when detecting the SD card
|
|
||||||
pinMode(LORA_CS, OUTPUT);
|
|
||||||
digitalWrite(LORA_CS, HIGH);
|
|
||||||
pinMode(SDCARD_CS, OUTPUT);
|
|
||||||
digitalWrite(SDCARD_CS, HIGH);
|
|
||||||
pinMode(TFT_CS, OUTPUT);
|
|
||||||
digitalWrite(TFT_CS, HIGH);
|
|
||||||
delay(100);
|
|
||||||
#elif defined(T_DECK_PRO)
|
|
||||||
pinMode(LORA_EN, OUTPUT);
|
|
||||||
digitalWrite(LORA_EN, HIGH);
|
|
||||||
pinMode(LORA_CS, OUTPUT);
|
|
||||||
digitalWrite(LORA_CS, HIGH);
|
|
||||||
pinMode(SDCARD_CS, OUTPUT);
|
|
||||||
digitalWrite(SDCARD_CS, HIGH);
|
|
||||||
pinMode(PIN_EINK_CS, OUTPUT);
|
|
||||||
digitalWrite(PIN_EINK_CS, HIGH);
|
|
||||||
#elif defined(T_LORA_PAGER)
|
|
||||||
pinMode(LORA_CS, OUTPUT);
|
|
||||||
digitalWrite(LORA_CS, HIGH);
|
|
||||||
pinMode(SDCARD_CS, OUTPUT);
|
|
||||||
digitalWrite(SDCARD_CS, HIGH);
|
|
||||||
pinMode(TFT_CS, OUTPUT);
|
|
||||||
digitalWrite(TFT_CS, HIGH);
|
|
||||||
pinMode(KB_INT, INPUT_PULLUP);
|
|
||||||
// io expander
|
|
||||||
io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL);
|
|
||||||
io.pinMode(EXPANDS_DRV_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_DRV_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_AMP_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_AMP_EN, LOW);
|
|
||||||
io.pinMode(EXPANDS_LORA_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_LORA_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_GPS_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_GPS_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_KB_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_KB_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_SD_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_SD_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_GPIO_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_SD_PULLEN, INPUT);
|
|
||||||
#elif defined(HACKADAY_COMMUNICATOR)
|
|
||||||
pinMode(KB_INT, INPUT);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
concurrency::hasBeenSetup = true;
|
concurrency::hasBeenSetup = true;
|
||||||
#if ARCH_PORTDUINO
|
|
||||||
SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0);
|
|
||||||
#else
|
|
||||||
SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
meshtastic_Config_DisplayConfig_OledType screen_model =
|
meshtastic_Config_DisplayConfig_OledType screen_model =
|
||||||
meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO;
|
meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO;
|
||||||
@@ -521,34 +484,11 @@ void setup()
|
|||||||
LOG_INFO("Wait for peripherals to stabilize");
|
LOG_INFO("Wait for peripherals to stabilize");
|
||||||
delay(PERIPHERAL_WARMUP_MS);
|
delay(PERIPHERAL_WARMUP_MS);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef BUTTON_PIN
|
|
||||||
#ifdef ARCH_ESP32
|
|
||||||
|
|
||||||
#if ESP_ARDUINO_VERSION_MAJOR >= 3
|
|
||||||
#ifdef BUTTON_NEED_PULLUP
|
|
||||||
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP);
|
|
||||||
#else
|
|
||||||
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
|
|
||||||
#endif
|
|
||||||
#else
|
|
||||||
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
|
|
||||||
#ifdef BUTTON_NEED_PULLUP
|
|
||||||
gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN));
|
|
||||||
delay(10);
|
|
||||||
#endif
|
|
||||||
#ifdef BUTTON_NEED_PULLUP2
|
|
||||||
gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2);
|
|
||||||
delay(10);
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
initSPI();
|
initSPI();
|
||||||
|
|
||||||
OSThread::setup();
|
OSThread::setup();
|
||||||
|
|
||||||
|
// TODO make this ifdef based on defined pins and move from main.cpp
|
||||||
#if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2)
|
#if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2)
|
||||||
// The ThinkNodes have their own blink logic
|
// The ThinkNodes have their own blink logic
|
||||||
// ledPeriodic = new Periodic("Blink", elecrowLedBlinker);
|
// ledPeriodic = new Periodic("Blink", elecrowLedBlinker);
|
||||||
@@ -648,7 +588,11 @@ void setup()
|
|||||||
sensor_detected = true;
|
sensor_detected = true;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
#ifdef DEBUG_PARTITION_TABLE
|
||||||
|
printPartitionTable();
|
||||||
|
#endif
|
||||||
|
#endif // ARCH_ESP32
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
// Don't init display if we don't have one or we are waking headless due to a timer event
|
// Don't init display if we don't have one or we are waking headless due to a timer event
|
||||||
if (wakeCause == ESP_SLEEP_WAKEUP_TIMER) {
|
if (wakeCause == ESP_SLEEP_WAKEUP_TIMER) {
|
||||||
@@ -922,6 +866,7 @@ void setup()
|
|||||||
}
|
}
|
||||||
#endif // HAS_SCREEN
|
#endif // HAS_SCREEN
|
||||||
|
|
||||||
|
// TODO Remove magic string
|
||||||
// setup TZ prior to time actions.
|
// setup TZ prior to time actions.
|
||||||
#if !MESHTASTIC_EXCLUDE_TZ
|
#if !MESHTASTIC_EXCLUDE_TZ
|
||||||
LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string
|
LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string
|
||||||
@@ -1005,180 +950,9 @@ void setup()
|
|||||||
nodeDB->hasWarned = true;
|
nodeDB->hasWarned = true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||||
// buttons are now inputBroker, so have to come after setupModules
|
if (inputBroker)
|
||||||
#if HAS_BUTTON
|
inputBroker->Init();
|
||||||
int pullup_sense = 0;
|
|
||||||
#ifdef INPUT_PULLUP_SENSE
|
|
||||||
// Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did
|
|
||||||
#ifdef BUTTON_SENSE_TYPE
|
|
||||||
pullup_sense = BUTTON_SENSE_TYPE;
|
|
||||||
#else
|
|
||||||
pullup_sense = INPUT_PULLUP_SENSE;
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
#if defined(ARCH_PORTDUINO)
|
|
||||||
|
|
||||||
if (portduino_config.userButtonPin.enabled) {
|
|
||||||
|
|
||||||
LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin);
|
|
||||||
UserButtonThread = new ButtonThread("UserButton");
|
|
||||||
if (screen) {
|
|
||||||
ButtonConfig config;
|
|
||||||
config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin;
|
|
||||||
config.activeLow = true;
|
|
||||||
config.activePullup = true;
|
|
||||||
config.pullupSense = INPUT_PULLUP;
|
|
||||||
config.intRoutine = []() {
|
|
||||||
UserButtonThread->userButton.tick();
|
|
||||||
UserButtonThread->setIntervalFromNow(0);
|
|
||||||
runASAP = true;
|
|
||||||
BaseType_t higherWake = 0;
|
|
||||||
mainDelay.interruptFromISR(&higherWake);
|
|
||||||
};
|
|
||||||
config.singlePress = INPUT_BROKER_USER_PRESS;
|
|
||||||
config.longPress = INPUT_BROKER_SELECT;
|
|
||||||
UserButtonThread->initButton(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef BUTTON_PIN_TOUCH
|
|
||||||
TouchButtonThread = new ButtonThread("BackButton");
|
|
||||||
ButtonConfig touchConfig;
|
|
||||||
touchConfig.pinNumber = BUTTON_PIN_TOUCH;
|
|
||||||
touchConfig.activeLow = true;
|
|
||||||
touchConfig.activePullup = true;
|
|
||||||
touchConfig.pullupSense = pullup_sense;
|
|
||||||
touchConfig.intRoutine = []() {
|
|
||||||
TouchButtonThread->userButton.tick();
|
|
||||||
TouchButtonThread->setIntervalFromNow(0);
|
|
||||||
runASAP = true;
|
|
||||||
BaseType_t higherWake = 0;
|
|
||||||
mainDelay.interruptFromISR(&higherWake);
|
|
||||||
};
|
|
||||||
touchConfig.singlePress = INPUT_BROKER_NONE;
|
|
||||||
touchConfig.longPress = INPUT_BROKER_BACK;
|
|
||||||
#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN)
|
|
||||||
// On T-Echo Plus the touch pad should only drive the backlight, not UI navigation/sounds
|
|
||||||
touchConfig.longPress = INPUT_BROKER_NONE;
|
|
||||||
touchConfig.suppressLeadUpSound = true;
|
|
||||||
touchConfig.onPress = []() {
|
|
||||||
touchBacklightWasOn = uiconfig.screen_brightness == 1;
|
|
||||||
if (!touchBacklightWasOn) {
|
|
||||||
digitalWrite(PIN_EINK_EN, HIGH);
|
|
||||||
}
|
|
||||||
touchBacklightActive = true;
|
|
||||||
};
|
|
||||||
touchConfig.onRelease = []() {
|
|
||||||
if (touchBacklightActive && !touchBacklightWasOn) {
|
|
||||||
digitalWrite(PIN_EINK_EN, LOW);
|
|
||||||
}
|
|
||||||
touchBacklightActive = false;
|
|
||||||
};
|
|
||||||
#endif
|
|
||||||
TouchButtonThread->initButton(touchConfig);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(CANCEL_BUTTON_PIN)
|
|
||||||
// Buttons. Moved here cause we need NodeDB to be initialized
|
|
||||||
CancelButtonThread = new ButtonThread("CancelButton");
|
|
||||||
ButtonConfig cancelConfig;
|
|
||||||
cancelConfig.pinNumber = CANCEL_BUTTON_PIN;
|
|
||||||
cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW;
|
|
||||||
cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP;
|
|
||||||
cancelConfig.pullupSense = pullup_sense;
|
|
||||||
cancelConfig.intRoutine = []() {
|
|
||||||
CancelButtonThread->userButton.tick();
|
|
||||||
CancelButtonThread->setIntervalFromNow(0);
|
|
||||||
runASAP = true;
|
|
||||||
BaseType_t higherWake = 0;
|
|
||||||
mainDelay.interruptFromISR(&higherWake);
|
|
||||||
};
|
|
||||||
cancelConfig.singlePress = INPUT_BROKER_CANCEL;
|
|
||||||
cancelConfig.longPress = INPUT_BROKER_SHUTDOWN;
|
|
||||||
cancelConfig.longPressTime = 4000;
|
|
||||||
CancelButtonThread->initButton(cancelConfig);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(ALT_BUTTON_PIN)
|
|
||||||
// Buttons. Moved here cause we need NodeDB to be initialized
|
|
||||||
BackButtonThread = new ButtonThread("BackButton");
|
|
||||||
ButtonConfig backConfig;
|
|
||||||
backConfig.pinNumber = ALT_BUTTON_PIN;
|
|
||||||
backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW;
|
|
||||||
backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP;
|
|
||||||
backConfig.pullupSense = pullup_sense;
|
|
||||||
backConfig.intRoutine = []() {
|
|
||||||
BackButtonThread->userButton.tick();
|
|
||||||
BackButtonThread->setIntervalFromNow(0);
|
|
||||||
runASAP = true;
|
|
||||||
BaseType_t higherWake = 0;
|
|
||||||
mainDelay.interruptFromISR(&higherWake);
|
|
||||||
};
|
|
||||||
backConfig.singlePress = INPUT_BROKER_ALT_PRESS;
|
|
||||||
backConfig.longPress = INPUT_BROKER_ALT_LONG;
|
|
||||||
backConfig.longPressTime = 500;
|
|
||||||
BackButtonThread->initButton(backConfig);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(BUTTON_PIN)
|
|
||||||
#if defined(USERPREFS_BUTTON_PIN)
|
|
||||||
int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN;
|
|
||||||
#else
|
|
||||||
int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN;
|
|
||||||
#endif
|
|
||||||
#ifndef BUTTON_ACTIVE_LOW
|
|
||||||
#define BUTTON_ACTIVE_LOW true
|
|
||||||
#endif
|
|
||||||
#ifndef BUTTON_ACTIVE_PULLUP
|
|
||||||
#define BUTTON_ACTIVE_PULLUP true
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Buttons. Moved here cause we need NodeDB to be initialized
|
|
||||||
// If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP
|
|
||||||
UserButtonThread = new ButtonThread("UserButton");
|
|
||||||
if (screen) {
|
|
||||||
ButtonConfig userConfig;
|
|
||||||
userConfig.pinNumber = (uint8_t)_pinNum;
|
|
||||||
userConfig.activeLow = BUTTON_ACTIVE_LOW;
|
|
||||||
userConfig.activePullup = BUTTON_ACTIVE_PULLUP;
|
|
||||||
userConfig.pullupSense = pullup_sense;
|
|
||||||
userConfig.intRoutine = []() {
|
|
||||||
UserButtonThread->userButton.tick();
|
|
||||||
UserButtonThread->setIntervalFromNow(0);
|
|
||||||
runASAP = true;
|
|
||||||
BaseType_t higherWake = 0;
|
|
||||||
mainDelay.interruptFromISR(&higherWake);
|
|
||||||
};
|
|
||||||
userConfig.singlePress = INPUT_BROKER_USER_PRESS;
|
|
||||||
userConfig.longPress = INPUT_BROKER_SELECT;
|
|
||||||
userConfig.longPressTime = 500;
|
|
||||||
userConfig.longLongPress = INPUT_BROKER_SHUTDOWN;
|
|
||||||
UserButtonThread->initButton(userConfig);
|
|
||||||
} else {
|
|
||||||
ButtonConfig userConfigNoScreen;
|
|
||||||
userConfigNoScreen.pinNumber = (uint8_t)_pinNum;
|
|
||||||
userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW;
|
|
||||||
userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP;
|
|
||||||
userConfigNoScreen.pullupSense = pullup_sense;
|
|
||||||
userConfigNoScreen.intRoutine = []() {
|
|
||||||
UserButtonThread->userButton.tick();
|
|
||||||
UserButtonThread->setIntervalFromNow(0);
|
|
||||||
runASAP = true;
|
|
||||||
BaseType_t higherWake = 0;
|
|
||||||
mainDelay.interruptFromISR(&higherWake);
|
|
||||||
};
|
|
||||||
userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS;
|
|
||||||
userConfigNoScreen.longPress = INPUT_BROKER_NONE;
|
|
||||||
userConfigNoScreen.longPressTime = 500;
|
|
||||||
userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN;
|
|
||||||
userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING;
|
|
||||||
userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE;
|
|
||||||
UserButtonThread->initButton(userConfigNoScreen);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
@@ -1217,252 +991,7 @@ void setup()
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef ARCH_PORTDUINO
|
initLoRa();
|
||||||
// as one can't use a function pointer to the class constructor:
|
|
||||||
auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
|
||||||
RADIOLIB_PIN_TYPE busy) {
|
|
||||||
switch (portduino_config.lora_module) {
|
|
||||||
case use_rf95:
|
|
||||||
return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy);
|
|
||||||
case use_sx1262:
|
|
||||||
return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy);
|
|
||||||
case use_sx1268:
|
|
||||||
return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy);
|
|
||||||
case use_sx1280:
|
|
||||||
return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy);
|
|
||||||
case use_lr1110:
|
|
||||||
return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy);
|
|
||||||
case use_lr1120:
|
|
||||||
return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy);
|
|
||||||
case use_lr1121:
|
|
||||||
return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy);
|
|
||||||
case use_llcc68:
|
|
||||||
return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy);
|
|
||||||
case use_simradio:
|
|
||||||
return (RadioInterface *)new SimRadio;
|
|
||||||
default:
|
|
||||||
assert(0); // shouldn't happen
|
|
||||||
return (RadioInterface *)nullptr;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(),
|
|
||||||
portduino_config.lora_spi_dev.c_str());
|
|
||||||
if (portduino_config.lora_spi_dev == "ch341") {
|
|
||||||
RadioLibHAL = ch341Hal;
|
|
||||||
} else {
|
|
||||||
RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
|
|
||||||
}
|
|
||||||
rIf =
|
|
||||||
loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin,
|
|
||||||
portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin);
|
|
||||||
|
|
||||||
if (!rIf->init()) {
|
|
||||||
LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str());
|
|
||||||
delete rIf;
|
|
||||||
rIf = NULL;
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
} else {
|
|
||||||
LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
#elif defined(HW_SPI1_DEVICE)
|
|
||||||
LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings);
|
|
||||||
#else // HW_SPI1_DEVICE
|
|
||||||
LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init)
|
|
||||||
#if defined(USE_STM32WLx)
|
|
||||||
if (!rIf) {
|
|
||||||
rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
|
||||||
if (!rIf->init()) {
|
|
||||||
LOG_WARN("No STM32WL radio");
|
|
||||||
delete rIf;
|
|
||||||
rIf = NULL;
|
|
||||||
} else {
|
|
||||||
LOG_INFO("STM32WL init success");
|
|
||||||
radioType = STM32WLx_RADIO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1
|
|
||||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
|
||||||
rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1);
|
|
||||||
if (!rIf->init()) {
|
|
||||||
LOG_WARN("No RF95 radio");
|
|
||||||
delete rIf;
|
|
||||||
rIf = NULL;
|
|
||||||
} else {
|
|
||||||
LOG_INFO("RF95 init success");
|
|
||||||
radioType = RF95_RADIO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1
|
|
||||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
|
||||||
auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
|
||||||
#ifdef SX126X_DIO3_TCXO_VOLTAGE
|
|
||||||
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
|
|
||||||
#endif
|
|
||||||
if (!sxIf->init()) {
|
|
||||||
LOG_WARN("No SX1262 radio");
|
|
||||||
delete sxIf;
|
|
||||||
rIf = NULL;
|
|
||||||
} else {
|
|
||||||
LOG_INFO("SX1262 init success");
|
|
||||||
rIf = sxIf;
|
|
||||||
radioType = SX1262_RADIO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL)
|
|
||||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
|
||||||
// try using the specified TCXO voltage
|
|
||||||
auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
|
||||||
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
|
|
||||||
if (!sxIf->init()) {
|
|
||||||
LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
|
||||||
delete sxIf;
|
|
||||||
rIf = NULL;
|
|
||||||
} else {
|
|
||||||
LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
|
||||||
rIf = sxIf;
|
|
||||||
radioType = SX1262_RADIO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
|
||||||
// If specified TCXO voltage fails, attempt to use DIO3 as a reference instead
|
|
||||||
rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
|
||||||
if (!rIf->init()) {
|
|
||||||
LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V");
|
|
||||||
delete rIf;
|
|
||||||
rIf = NULL;
|
|
||||||
} else {
|
|
||||||
LOG_INFO("SX1262 init success, XTAL, Vref 0.0V");
|
|
||||||
radioType = SX1262_RADIO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(USE_SX1268)
|
|
||||||
#if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL)
|
|
||||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
|
||||||
// try using the specified TCXO voltage
|
|
||||||
auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
|
||||||
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
|
|
||||||
if (!sxIf->init()) {
|
|
||||||
LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
|
||||||
delete sxIf;
|
|
||||||
rIf = NULL;
|
|
||||||
} else {
|
|
||||||
LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
|
||||||
rIf = sxIf;
|
|
||||||
radioType = SX1268_RADIO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
|
||||||
rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
|
||||||
if (!rIf->init()) {
|
|
||||||
LOG_WARN("No SX1268 radio");
|
|
||||||
delete rIf;
|
|
||||||
rIf = NULL;
|
|
||||||
} else {
|
|
||||||
LOG_INFO("SX1268 init success");
|
|
||||||
radioType = SX1268_RADIO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(USE_LLCC68)
|
|
||||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
|
||||||
rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
|
||||||
if (!rIf->init()) {
|
|
||||||
LOG_WARN("No LLCC68 radio");
|
|
||||||
delete rIf;
|
|
||||||
rIf = NULL;
|
|
||||||
} else {
|
|
||||||
LOG_INFO("LLCC68 init success");
|
|
||||||
radioType = LLCC68_RADIO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1
|
|
||||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
|
||||||
rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN);
|
|
||||||
if (!rIf->init()) {
|
|
||||||
LOG_WARN("No LR1110 radio");
|
|
||||||
delete rIf;
|
|
||||||
rIf = NULL;
|
|
||||||
} else {
|
|
||||||
LOG_INFO("LR1110 init success");
|
|
||||||
radioType = LR1110_RADIO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1
|
|
||||||
if (!rIf) {
|
|
||||||
rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN);
|
|
||||||
if (!rIf->init()) {
|
|
||||||
LOG_WARN("No LR1120 radio");
|
|
||||||
delete rIf;
|
|
||||||
rIf = NULL;
|
|
||||||
} else {
|
|
||||||
LOG_INFO("LR1120 init success");
|
|
||||||
radioType = LR1120_RADIO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1
|
|
||||||
if (!rIf) {
|
|
||||||
rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN);
|
|
||||||
if (!rIf->init()) {
|
|
||||||
LOG_WARN("No LR1121 radio");
|
|
||||||
delete rIf;
|
|
||||||
rIf = NULL;
|
|
||||||
} else {
|
|
||||||
LOG_INFO("LR1121 init success");
|
|
||||||
radioType = LR1121_RADIO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1
|
|
||||||
if (!rIf) {
|
|
||||||
rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY);
|
|
||||||
if (!rIf->init()) {
|
|
||||||
LOG_WARN("No SX1280 radio");
|
|
||||||
delete rIf;
|
|
||||||
rIf = NULL;
|
|
||||||
} else {
|
|
||||||
LOG_INFO("SX1280 init success");
|
|
||||||
radioType = SX1280_RADIO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// check if the radio chip matches the selected region
|
|
||||||
if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && rIf && (!rIf->wideLora())) {
|
|
||||||
LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset");
|
|
||||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
||||||
nodeDB->saveToDisk(SEGMENT_CONFIG);
|
|
||||||
|
|
||||||
if (!rIf->reconfigure()) {
|
|
||||||
LOG_WARN("Reconfigure failed, rebooting");
|
|
||||||
if (screen) {
|
|
||||||
screen->showSimpleBanner("Rebooting...");
|
|
||||||
}
|
|
||||||
rebootAtMsec = millis() + 5000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation)
|
lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation)
|
||||||
|
|
||||||
@@ -1551,6 +1080,7 @@ bool suppressRebootBanner; // If true, suppress "Rebooting..." overlay (used for
|
|||||||
// This will suppress the current delay and instead try to run ASAP.
|
// This will suppress the current delay and instead try to run ASAP.
|
||||||
bool runASAP;
|
bool runASAP;
|
||||||
|
|
||||||
|
// TODO find better home than main.cpp
|
||||||
extern meshtastic_DeviceMetadata getDeviceMetadata()
|
extern meshtastic_DeviceMetadata getDeviceMetadata()
|
||||||
{
|
{
|
||||||
meshtastic_DeviceMetadata deviceMetadata;
|
meshtastic_DeviceMetadata deviceMetadata;
|
||||||
@@ -1651,7 +1181,43 @@ void loop()
|
|||||||
if (inputBroker)
|
if (inputBroker)
|
||||||
inputBroker->processInputEventQueue();
|
inputBroker->processInputEventQueue();
|
||||||
#endif
|
#endif
|
||||||
#if ARCH_PORTDUINO && HAS_TFT
|
#if ARCH_PORTDUINO
|
||||||
|
if (portduino_config.lora_spi_dev == "ch341" && ch341Hal != nullptr) {
|
||||||
|
ch341Hal->checkError();
|
||||||
|
}
|
||||||
|
if (portduino_status.LoRa_in_error && rebootAtMsec == 0) {
|
||||||
|
LOG_ERROR("LoRa in error detected, attempting to recover");
|
||||||
|
if (rIf != nullptr) {
|
||||||
|
delete rIf;
|
||||||
|
rIf = nullptr;
|
||||||
|
}
|
||||||
|
if (portduino_config.lora_spi_dev == "ch341") {
|
||||||
|
if (ch341Hal != nullptr) {
|
||||||
|
delete ch341Hal;
|
||||||
|
ch341Hal = nullptr;
|
||||||
|
sleep(3);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid,
|
||||||
|
portduino_config.lora_usb_pid);
|
||||||
|
} catch (std::exception &e) {
|
||||||
|
std::cerr << e.what() << std::endl;
|
||||||
|
std::cerr << "Could not initialize CH341 device!" << std::endl;
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (initLoRa()) {
|
||||||
|
router->addInterface(rIf);
|
||||||
|
portduino_status.LoRa_in_error = false;
|
||||||
|
} else {
|
||||||
|
LOG_WARN("Reconfigure failed, rebooting");
|
||||||
|
if (screen) {
|
||||||
|
screen->showSimpleBanner("Rebooting...");
|
||||||
|
}
|
||||||
|
rebootAtMsec = millis() + 25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if HAS_TFT
|
||||||
if (screen && portduino_config.displayPanel == x11 &&
|
if (screen && portduino_config.displayPanel == x11 &&
|
||||||
config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||||
auto dispdev = screen->getDisplayDevice();
|
auto dispdev = screen->getDisplayDevice();
|
||||||
@@ -1659,6 +1225,7 @@ void loop()
|
|||||||
static_cast<TFTDisplay *>(dispdev)->sdlLoop();
|
static_cast<TFTDisplay *>(dispdev)->sdlLoop();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
#if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE
|
#if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE
|
||||||
messageStoreAutosaveTick();
|
messageStoreAutosaveTick();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
12
src/main.h
12
src/main.h
@@ -26,8 +26,8 @@ extern NRF52Bluetooth *nrf52Bluetooth;
|
|||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
extern HardwareSPI *DisplaySPI;
|
extern HardwareSPI *DisplaySPI;
|
||||||
extern HardwareSPI *LoraSPI;
|
extern HardwareSPI *LoraSPI;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
extern ScanI2C::DeviceAddress screen_found;
|
extern ScanI2C::DeviceAddress screen_found;
|
||||||
extern ScanI2C::DeviceAddress cardkb_found;
|
extern ScanI2C::DeviceAddress cardkb_found;
|
||||||
extern uint8_t kb_model;
|
extern uint8_t kb_model;
|
||||||
@@ -47,16 +47,16 @@ extern bool isUSBPowered;
|
|||||||
extern Adafruit_DRV2605 drv;
|
extern Adafruit_DRV2605 drv;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAS_PCA9557
|
||||||
|
#include <PCA9557.h>
|
||||||
|
extern PCA9557 io;
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef HAS_I2S
|
#ifdef HAS_I2S
|
||||||
#include "AudioThread.h"
|
#include "AudioThread.h"
|
||||||
extern AudioThread *audioThread;
|
extern AudioThread *audioThread;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef ELECROW_ThinkNode_M5
|
|
||||||
#include <PCA9557.h>
|
|
||||||
extern PCA9557 io;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef HAS_UDP_MULTICAST
|
#ifdef HAS_UDP_MULTICAST
|
||||||
#include "mesh/udp/UdpMulticastHandler.h"
|
#include "mesh/udp/UdpMulticastHandler.h"
|
||||||
extern UdpMulticastHandler *udpHandler;
|
extern UdpMulticastHandler *udpHandler;
|
||||||
|
|||||||
@@ -61,11 +61,6 @@ bool CryptoEngine::regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
void CryptoEngine::clearKeys()
|
|
||||||
{
|
|
||||||
memset(public_key, 0, sizeof(public_key));
|
|
||||||
memset(private_key, 0, sizeof(private_key));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypt a packet's payload using a key generated with Curve25519 and SHA256
|
* Encrypt a packet's payload using a key generated with Curve25519 and SHA256
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ class CryptoEngine
|
|||||||
virtual bool regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey);
|
virtual bool regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
void clearKeys();
|
|
||||||
void setDHPrivateKey(uint8_t *_private_key);
|
void setDHPrivateKey(uint8_t *_private_key);
|
||||||
virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic,
|
virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic,
|
||||||
uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut);
|
uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut);
|
||||||
|
|||||||
@@ -91,10 +91,21 @@ template <typename T> bool LR11x0Interface<T>::init()
|
|||||||
LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz");
|
LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz");
|
||||||
#endif
|
#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);
|
int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
||||||
|
|
||||||
|
// Retry if we get SPI command failed - some units need extra TCXO stabilization time
|
||||||
|
if (res == RADIOLIB_ERR_SPI_CMD_FAILED) {
|
||||||
|
LOG_WARN("LR11x0 init failed with %d (SPI_CMD_FAILED), retrying after delay...", res);
|
||||||
|
delay(100);
|
||||||
|
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
||||||
|
}
|
||||||
|
|
||||||
// \todo Display actual typename of the adapter, not just `LR11x0`
|
// \todo Display actual typename of the adapter, not just `LR11x0`
|
||||||
LOG_INFO("LR11x0 init result %d", res);
|
LOG_INFO("LR11x0 init result %d", res);
|
||||||
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND)
|
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
LR11x0VersionInfo_t version;
|
LR11x0VersionInfo_t version;
|
||||||
@@ -159,7 +170,7 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
|
|||||||
if (err != RADIOLIB_ERR_NONE)
|
if (err != RADIOLIB_ERR_NONE)
|
||||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||||
|
|
||||||
err = lora.setCodingRate(cr);
|
err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it
|
||||||
if (err != RADIOLIB_ERR_NONE)
|
if (err != RADIOLIB_ERR_NONE)
|
||||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||||
|
|
||||||
|
|||||||
@@ -22,4 +22,99 @@ struct RegionInfo {
|
|||||||
extern const RegionInfo regions[];
|
extern const RegionInfo regions[];
|
||||||
extern const RegionInfo *myRegion;
|
extern const RegionInfo *myRegion;
|
||||||
|
|
||||||
extern void initRegion();
|
extern void initRegion();
|
||||||
|
|
||||||
|
static inline float bwCodeToKHz(uint16_t bwCode)
|
||||||
|
{
|
||||||
|
if (bwCode == 31)
|
||||||
|
return 31.25f;
|
||||||
|
if (bwCode == 62)
|
||||||
|
return 62.5f;
|
||||||
|
if (bwCode == 200)
|
||||||
|
return 203.125f;
|
||||||
|
if (bwCode == 400)
|
||||||
|
return 406.25f;
|
||||||
|
if (bwCode == 800)
|
||||||
|
return 812.5f;
|
||||||
|
if (bwCode == 1600)
|
||||||
|
return 1625.0f;
|
||||||
|
return (float)bwCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint16_t bwKHzToCode(float bwKHz)
|
||||||
|
{
|
||||||
|
if (bwKHz > 31.24f && bwKHz < 31.26f)
|
||||||
|
return 31;
|
||||||
|
if (bwKHz > 62.49f && bwKHz < 62.51f)
|
||||||
|
return 62;
|
||||||
|
if (bwKHz > 203.12f && bwKHz < 203.13f)
|
||||||
|
return 200;
|
||||||
|
if (bwKHz > 406.24f && bwKHz < 406.26f)
|
||||||
|
return 400;
|
||||||
|
if (bwKHz > 812.49f && bwKHz < 812.51f)
|
||||||
|
return 800;
|
||||||
|
if (bwKHz > 1624.99f && bwKHz < 1625.01f)
|
||||||
|
return 1600;
|
||||||
|
return (uint16_t)(bwKHz + 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void modemPresetToParams(meshtastic_Config_LoRaConfig_ModemPreset preset, bool wideLora, float &bwKHz, uint8_t &sf,
|
||||||
|
uint8_t &cr)
|
||||||
|
{
|
||||||
|
switch (preset) {
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
|
||||||
|
bwKHz = wideLora ? 1625.0f : 500.0f;
|
||||||
|
cr = 5;
|
||||||
|
sf = 7;
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
|
||||||
|
bwKHz = wideLora ? 812.5f : 250.0f;
|
||||||
|
cr = 5;
|
||||||
|
sf = 7;
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
|
||||||
|
bwKHz = wideLora ? 812.5f : 250.0f;
|
||||||
|
cr = 5;
|
||||||
|
sf = 8;
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
|
||||||
|
bwKHz = wideLora ? 812.5f : 250.0f;
|
||||||
|
cr = 5;
|
||||||
|
sf = 9;
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
|
||||||
|
bwKHz = wideLora ? 812.5f : 250.0f;
|
||||||
|
cr = 5;
|
||||||
|
sf = 10;
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
|
||||||
|
bwKHz = wideLora ? 1625.0f : 500.0f;
|
||||||
|
cr = 8;
|
||||||
|
sf = 11;
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
|
||||||
|
bwKHz = wideLora ? 406.25f : 125.0f;
|
||||||
|
cr = 8;
|
||||||
|
sf = 11;
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
|
||||||
|
bwKHz = wideLora ? 406.25f : 125.0f;
|
||||||
|
cr = 8;
|
||||||
|
sf = 12;
|
||||||
|
break;
|
||||||
|
default: // LONG_FAST (or illegal)
|
||||||
|
bwKHz = wideLora ? 812.5f : 250.0f;
|
||||||
|
cr = 5;
|
||||||
|
sf = 11;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline float modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset preset, bool wideLora)
|
||||||
|
{
|
||||||
|
float bwKHz = 0;
|
||||||
|
uint8_t sf = 0;
|
||||||
|
uint8_t cr = 0;
|
||||||
|
modemPresetToParams(preset, wideLora, bwKHz, sf, cr);
|
||||||
|
return bwKHz;
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "PacketHistory.h"
|
#include "PacketHistory.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
|
#include "RadioInterface.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "SPILock.h"
|
#include "SPILock.h"
|
||||||
#include "SafeFile.h"
|
#include "SafeFile.h"
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <pb_decode.h>
|
#include <pb_decode.h>
|
||||||
#include <pb_encode.h>
|
#include <pb_encode.h>
|
||||||
|
#include <power/PowerHAL.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
@@ -822,16 +824,10 @@ void NodeDB::installDefaultModuleConfig()
|
|||||||
moduleConfig.external_notification.output_ms = 500;
|
moduleConfig.external_notification.output_ms = 500;
|
||||||
moduleConfig.external_notification.nag_timeout = 2;
|
moduleConfig.external_notification.nag_timeout = 2;
|
||||||
#endif
|
#endif
|
||||||
#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || \
|
#if defined(LED_NOTIFICATION)
|
||||||
defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M6)
|
|
||||||
// Default to PIN_LED2 for external notification output (LED color depends on device variant)
|
|
||||||
moduleConfig.external_notification.enabled = true;
|
moduleConfig.external_notification.enabled = true;
|
||||||
moduleConfig.external_notification.output = PIN_LED2;
|
moduleConfig.external_notification.output = LED_NOTIFICATION;
|
||||||
#if defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3)
|
moduleConfig.external_notification.active = LED_STATE_ON;
|
||||||
moduleConfig.external_notification.active = false;
|
|
||||||
#else
|
|
||||||
moduleConfig.external_notification.active = true;
|
|
||||||
#endif
|
|
||||||
moduleConfig.external_notification.alert_message = true;
|
moduleConfig.external_notification.alert_message = true;
|
||||||
moduleConfig.external_notification.output_ms = 1000;
|
moduleConfig.external_notification.output_ms = 1000;
|
||||||
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
|
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
|
||||||
@@ -855,15 +851,6 @@ void NodeDB::installDefaultModuleConfig()
|
|||||||
moduleConfig.external_notification.output_ms = 100;
|
moduleConfig.external_notification.output_ms = 100;
|
||||||
moduleConfig.external_notification.active = true;
|
moduleConfig.external_notification.active = true;
|
||||||
#endif
|
#endif
|
||||||
#ifdef ELECROW_ThinkNode_M1
|
|
||||||
// Default to Elecrow USER_LED (blue)
|
|
||||||
moduleConfig.external_notification.enabled = true;
|
|
||||||
moduleConfig.external_notification.output = USER_LED;
|
|
||||||
moduleConfig.external_notification.active = true;
|
|
||||||
moduleConfig.external_notification.alert_message = true;
|
|
||||||
moduleConfig.external_notification.output_ms = 1000;
|
|
||||||
moduleConfig.external_notification.nag_timeout = 60;
|
|
||||||
#endif
|
|
||||||
#ifdef T_LORA_PAGER
|
#ifdef T_LORA_PAGER
|
||||||
moduleConfig.canned_message.updown1_enabled = true;
|
moduleConfig.canned_message.updown1_enabled = true;
|
||||||
moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A;
|
moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A;
|
||||||
@@ -1297,6 +1284,13 @@ void NodeDB::loadFromDisk()
|
|||||||
LOG_INFO("Loaded saved config version %d", config.version);
|
LOG_INFO("Loaded saved config version %d", config.version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Coerce LoRa config fields derived from presets while bootstrapping.
|
||||||
|
// Some clients/UI components display bandwidth/spread_factor directly from config even in preset mode.
|
||||||
|
if (config.has_lora && config.lora.use_preset) {
|
||||||
|
RadioInterface::bootstrapLoRaConfigFromPreset(config.lora);
|
||||||
|
}
|
||||||
|
|
||||||
if (backupSecurity.private_key.size > 0) {
|
if (backupSecurity.private_key.size > 0) {
|
||||||
LOG_DEBUG("Restoring backup of security config");
|
LOG_DEBUG("Restoring backup of security config");
|
||||||
config.security = backupSecurity;
|
config.security = backupSecurity;
|
||||||
@@ -1418,6 +1412,14 @@ void NodeDB::loadFromDisk()
|
|||||||
bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct,
|
bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct,
|
||||||
bool fullAtomic)
|
bool fullAtomic)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
||||||
|
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
||||||
|
if (!powerHAL_isPowerLevelSafe()) {
|
||||||
|
LOG_ERROR("Error: trying to saveProto() on unsafe device power level.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool okay = false;
|
bool okay = false;
|
||||||
#ifdef FSCom
|
#ifdef FSCom
|
||||||
auto f = SafeFile(filename, fullAtomic);
|
auto f = SafeFile(filename, fullAtomic);
|
||||||
@@ -1444,6 +1446,14 @@ bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_
|
|||||||
|
|
||||||
bool NodeDB::saveChannelsToDisk()
|
bool NodeDB::saveChannelsToDisk()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
||||||
|
// and all writes will fail anyway.
|
||||||
|
if (!powerHAL_isPowerLevelSafe()) {
|
||||||
|
LOG_ERROR("Error: trying to saveChannelsToDisk() on unsafe device power level.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef FSCom
|
#ifdef FSCom
|
||||||
spiLock->lock();
|
spiLock->lock();
|
||||||
FSCom.mkdir("/prefs");
|
FSCom.mkdir("/prefs");
|
||||||
@@ -1454,6 +1464,14 @@ bool NodeDB::saveChannelsToDisk()
|
|||||||
|
|
||||||
bool NodeDB::saveDeviceStateToDisk()
|
bool NodeDB::saveDeviceStateToDisk()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
||||||
|
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
||||||
|
if (!powerHAL_isPowerLevelSafe()) {
|
||||||
|
LOG_ERROR("Error: trying to saveDeviceStateToDisk() on unsafe device power level.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef FSCom
|
#ifdef FSCom
|
||||||
spiLock->lock();
|
spiLock->lock();
|
||||||
FSCom.mkdir("/prefs");
|
FSCom.mkdir("/prefs");
|
||||||
@@ -1466,6 +1484,14 @@ bool NodeDB::saveDeviceStateToDisk()
|
|||||||
|
|
||||||
bool NodeDB::saveNodeDatabaseToDisk()
|
bool NodeDB::saveNodeDatabaseToDisk()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
||||||
|
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
||||||
|
if (!powerHAL_isPowerLevelSafe()) {
|
||||||
|
LOG_ERROR("Error: trying to saveNodeDatabaseToDisk() on unsafe device power level.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef FSCom
|
#ifdef FSCom
|
||||||
spiLock->lock();
|
spiLock->lock();
|
||||||
FSCom.mkdir("/prefs");
|
FSCom.mkdir("/prefs");
|
||||||
@@ -1478,6 +1504,14 @@ bool NodeDB::saveNodeDatabaseToDisk()
|
|||||||
|
|
||||||
bool NodeDB::saveToDiskNoRetry(int saveWhat)
|
bool NodeDB::saveToDiskNoRetry(int saveWhat)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
||||||
|
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
||||||
|
if (!powerHAL_isPowerLevelSafe()) {
|
||||||
|
LOG_ERROR("Error: trying to saveToDiskNoRetry() on unsafe device power level.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool success = true;
|
bool success = true;
|
||||||
#ifdef FSCom
|
#ifdef FSCom
|
||||||
spiLock->lock();
|
spiLock->lock();
|
||||||
@@ -1533,6 +1567,14 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
|
|||||||
bool NodeDB::saveToDisk(int saveWhat)
|
bool NodeDB::saveToDisk(int saveWhat)
|
||||||
{
|
{
|
||||||
LOG_DEBUG("Save to disk %d", saveWhat);
|
LOG_DEBUG("Save to disk %d", saveWhat);
|
||||||
|
|
||||||
|
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
||||||
|
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
||||||
|
if (!powerHAL_isPowerLevelSafe()) {
|
||||||
|
LOG_ERROR("Error: trying to saveToDisk() on unsafe device power level.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool success = saveToDiskNoRetry(saveWhat);
|
bool success = saveToDiskNoRetry(saveWhat);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
@@ -2190,7 +2232,10 @@ void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, co
|
|||||||
|
|
||||||
// Currently portuino is mostly used for simulation. Make sure the user notices something really bad happened
|
// Currently portuino is mostly used for simulation. Make sure the user notices something really bad happened
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
LOG_ERROR("A critical failure occurred, portduino is exiting");
|
LOG_ERROR("A critical failure occurred");
|
||||||
exit(2);
|
// TODO: Determine if other critical errors should also cause an immediate exit
|
||||||
|
if (code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE ||
|
||||||
|
code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE)
|
||||||
|
exit(2);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,6 +177,9 @@ bool RF95Interface::init()
|
|||||||
|
|
||||||
int res = lora->begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength);
|
int res = lora->begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength);
|
||||||
LOG_INFO("RF95 init result %d", res);
|
LOG_INFO("RF95 init result %d", res);
|
||||||
|
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
|
||||||
|
return false;
|
||||||
|
|
||||||
LOG_INFO("Frequency set to %f", getFreq());
|
LOG_INFO("Frequency set to %f", getFreq());
|
||||||
LOG_INFO("Bandwidth set to %f", bw);
|
LOG_INFO("Bandwidth set to %f", bw);
|
||||||
LOG_INFO("Power output set to %d", power);
|
LOG_INFO("Power output set to %d", power);
|
||||||
|
|||||||
@@ -1,17 +1,36 @@
|
|||||||
#include "RadioInterface.h"
|
#include "RadioInterface.h"
|
||||||
#include "Channels.h"
|
#include "Channels.h"
|
||||||
#include "DisplayFormatters.h"
|
#include "DisplayFormatters.h"
|
||||||
|
#include "LLCC68Interface.h"
|
||||||
|
#include "LR1110Interface.h"
|
||||||
|
#include "LR1120Interface.h"
|
||||||
|
#include "LR1121Interface.h"
|
||||||
#include "MeshRadio.h"
|
#include "MeshRadio.h"
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
|
#include "RF95Interface.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
|
#include "SX1262Interface.h"
|
||||||
|
#include "SX1268Interface.h"
|
||||||
|
#include "SX1280Interface.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "detect/LoRaRadioType.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <pb_decode.h>
|
#include <pb_decode.h>
|
||||||
#include <pb_encode.h>
|
#include <pb_encode.h>
|
||||||
|
|
||||||
|
#ifdef ARCH_PORTDUINO
|
||||||
|
#include "platform/portduino/PortduinoGlue.h"
|
||||||
|
#include "platform/portduino/SimRadio.h"
|
||||||
|
#include "platform/portduino/USBHal.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef ARCH_STM32WL
|
||||||
|
#include "STM32WLE5JCInterface.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
// Calculate 2^n without calling pow()
|
// Calculate 2^n without calling pow()
|
||||||
uint32_t pow_of_2(uint32_t n)
|
uint32_t pow_of_2(uint32_t n)
|
||||||
{
|
{
|
||||||
@@ -205,6 +224,281 @@ bool RadioInterface::uses_default_frequency_slot = true;
|
|||||||
|
|
||||||
static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1];
|
static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1];
|
||||||
|
|
||||||
|
// Global LoRa radio type
|
||||||
|
LoRaRadioType radioType = NO_RADIO;
|
||||||
|
|
||||||
|
extern RadioInterface *rIf;
|
||||||
|
extern RadioLibHal *RadioLibHAL;
|
||||||
|
#if defined(HW_SPI1_DEVICE) && defined(ARCH_ESP32)
|
||||||
|
extern SPIClass SPI1;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool initLoRa()
|
||||||
|
{
|
||||||
|
if (rIf != nullptr) {
|
||||||
|
delete rIf;
|
||||||
|
rIf = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ARCH_PORTDUINO
|
||||||
|
SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0);
|
||||||
|
#else
|
||||||
|
SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef ARCH_PORTDUINO
|
||||||
|
// as one can't use a function pointer to the class constructor:
|
||||||
|
auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
||||||
|
RADIOLIB_PIN_TYPE busy) {
|
||||||
|
switch (portduino_config.lora_module) {
|
||||||
|
case use_rf95:
|
||||||
|
return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy);
|
||||||
|
case use_sx1262:
|
||||||
|
return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy);
|
||||||
|
case use_sx1268:
|
||||||
|
return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy);
|
||||||
|
case use_sx1280:
|
||||||
|
return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy);
|
||||||
|
case use_lr1110:
|
||||||
|
return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy);
|
||||||
|
case use_lr1120:
|
||||||
|
return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy);
|
||||||
|
case use_lr1121:
|
||||||
|
return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy);
|
||||||
|
case use_llcc68:
|
||||||
|
return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy);
|
||||||
|
case use_simradio:
|
||||||
|
return (RadioInterface *)new SimRadio;
|
||||||
|
default:
|
||||||
|
assert(0); // shouldn't happen
|
||||||
|
return (RadioInterface *)nullptr;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(),
|
||||||
|
portduino_config.lora_spi_dev.c_str());
|
||||||
|
if (portduino_config.lora_spi_dev == "ch341") {
|
||||||
|
RadioLibHAL = ch341Hal;
|
||||||
|
} else {
|
||||||
|
if (RadioLibHAL != nullptr) {
|
||||||
|
delete RadioLibHAL;
|
||||||
|
RadioLibHAL = nullptr;
|
||||||
|
}
|
||||||
|
RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
|
||||||
|
}
|
||||||
|
rIf =
|
||||||
|
loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin,
|
||||||
|
portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin);
|
||||||
|
|
||||||
|
if (!rIf->init()) {
|
||||||
|
LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str());
|
||||||
|
delete rIf;
|
||||||
|
rIf = NULL;
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
} else {
|
||||||
|
LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
#elif defined(HW_SPI1_DEVICE)
|
||||||
|
LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings);
|
||||||
|
#else // HW_SPI1_DEVICE
|
||||||
|
LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init)
|
||||||
|
#if defined(USE_STM32WLx)
|
||||||
|
if (!rIf) {
|
||||||
|
rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
||||||
|
if (!rIf->init()) {
|
||||||
|
LOG_WARN("No STM32WL radio");
|
||||||
|
delete rIf;
|
||||||
|
rIf = NULL;
|
||||||
|
} else {
|
||||||
|
LOG_INFO("STM32WL init success");
|
||||||
|
radioType = STM32WLx_RADIO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1
|
||||||
|
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||||
|
rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1);
|
||||||
|
if (!rIf->init()) {
|
||||||
|
LOG_WARN("No RF95 radio");
|
||||||
|
delete rIf;
|
||||||
|
rIf = NULL;
|
||||||
|
} else {
|
||||||
|
LOG_INFO("RF95 init success");
|
||||||
|
radioType = RF95_RADIO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1
|
||||||
|
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||||
|
auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
||||||
|
#ifdef SX126X_DIO3_TCXO_VOLTAGE
|
||||||
|
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
|
||||||
|
#endif
|
||||||
|
if (!sxIf->init()) {
|
||||||
|
LOG_WARN("No SX1262 radio");
|
||||||
|
delete sxIf;
|
||||||
|
rIf = NULL;
|
||||||
|
} else {
|
||||||
|
LOG_INFO("SX1262 init success");
|
||||||
|
rIf = sxIf;
|
||||||
|
radioType = SX1262_RADIO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL)
|
||||||
|
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||||
|
// try using the specified TCXO voltage
|
||||||
|
auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
||||||
|
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
|
||||||
|
if (!sxIf->init()) {
|
||||||
|
LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
||||||
|
delete sxIf;
|
||||||
|
rIf = NULL;
|
||||||
|
} else {
|
||||||
|
LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
||||||
|
rIf = sxIf;
|
||||||
|
radioType = SX1262_RADIO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||||
|
// If specified TCXO voltage fails, attempt to use DIO3 as a reference instead
|
||||||
|
rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
||||||
|
if (!rIf->init()) {
|
||||||
|
LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V");
|
||||||
|
delete rIf;
|
||||||
|
rIf = NULL;
|
||||||
|
} else {
|
||||||
|
LOG_INFO("SX1262 init success, XTAL, Vref 0.0V");
|
||||||
|
radioType = SX1262_RADIO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(USE_SX1268)
|
||||||
|
#if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL)
|
||||||
|
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||||
|
// try using the specified TCXO voltage
|
||||||
|
auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
||||||
|
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
|
||||||
|
if (!sxIf->init()) {
|
||||||
|
LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
||||||
|
delete sxIf;
|
||||||
|
rIf = NULL;
|
||||||
|
} else {
|
||||||
|
LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
||||||
|
rIf = sxIf;
|
||||||
|
radioType = SX1268_RADIO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||||
|
rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
||||||
|
if (!rIf->init()) {
|
||||||
|
LOG_WARN("No SX1268 radio");
|
||||||
|
delete rIf;
|
||||||
|
rIf = NULL;
|
||||||
|
} else {
|
||||||
|
LOG_INFO("SX1268 init success");
|
||||||
|
radioType = SX1268_RADIO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(USE_LLCC68)
|
||||||
|
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||||
|
rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
|
||||||
|
if (!rIf->init()) {
|
||||||
|
LOG_WARN("No LLCC68 radio");
|
||||||
|
delete rIf;
|
||||||
|
rIf = NULL;
|
||||||
|
} else {
|
||||||
|
LOG_INFO("LLCC68 init success");
|
||||||
|
radioType = LLCC68_RADIO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1
|
||||||
|
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||||
|
rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN);
|
||||||
|
if (!rIf->init()) {
|
||||||
|
LOG_WARN("No LR1110 radio");
|
||||||
|
delete rIf;
|
||||||
|
rIf = NULL;
|
||||||
|
} else {
|
||||||
|
LOG_INFO("LR1110 init success");
|
||||||
|
radioType = LR1110_RADIO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1
|
||||||
|
if (!rIf) {
|
||||||
|
rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN);
|
||||||
|
if (!rIf->init()) {
|
||||||
|
LOG_WARN("No LR1120 radio");
|
||||||
|
delete rIf;
|
||||||
|
rIf = NULL;
|
||||||
|
} else {
|
||||||
|
LOG_INFO("LR1120 init success");
|
||||||
|
radioType = LR1120_RADIO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1
|
||||||
|
if (!rIf) {
|
||||||
|
rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN);
|
||||||
|
if (!rIf->init()) {
|
||||||
|
LOG_WARN("No LR1121 radio");
|
||||||
|
delete rIf;
|
||||||
|
rIf = NULL;
|
||||||
|
} else {
|
||||||
|
LOG_INFO("LR1121 init success");
|
||||||
|
radioType = LR1121_RADIO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1
|
||||||
|
if (!rIf) {
|
||||||
|
rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY);
|
||||||
|
if (!rIf->init()) {
|
||||||
|
LOG_WARN("No SX1280 radio");
|
||||||
|
delete rIf;
|
||||||
|
rIf = NULL;
|
||||||
|
} else {
|
||||||
|
LOG_INFO("SX1280 init success");
|
||||||
|
radioType = SX1280_RADIO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// check if the radio chip matches the selected region
|
||||||
|
if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && rIf && (!rIf->wideLora())) {
|
||||||
|
LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset");
|
||||||
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
||||||
|
nodeDB->saveToDisk(SEGMENT_CONFIG);
|
||||||
|
|
||||||
|
if (rIf && !rIf->reconfigure()) {
|
||||||
|
LOG_WARN("Reconfigure failed, rebooting");
|
||||||
|
if (screen) {
|
||||||
|
screen->showSimpleBanner("Rebooting...");
|
||||||
|
}
|
||||||
|
rebootAtMsec = millis() + 5000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rIf != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void initRegion()
|
void initRegion()
|
||||||
{
|
{
|
||||||
const RegionInfo *r = regions;
|
const RegionInfo *r = regions;
|
||||||
@@ -220,6 +514,34 @@ void initRegion()
|
|||||||
myRegion = r;
|
myRegion = r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RadioInterface::bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig)
|
||||||
|
{
|
||||||
|
if (!loraConfig.use_preset) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find region info to determine whether "wide" LoRa is permitted (2.4 GHz uses wider bandwidth codes).
|
||||||
|
const RegionInfo *r = regions;
|
||||||
|
for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != loraConfig.region; r++)
|
||||||
|
;
|
||||||
|
|
||||||
|
const bool regionWideLora = r->wideLora;
|
||||||
|
|
||||||
|
float bwKHz = 0;
|
||||||
|
uint8_t sf = 0;
|
||||||
|
uint8_t cr = 0;
|
||||||
|
modemPresetToParams(loraConfig.modem_preset, regionWideLora, bwKHz, sf, cr);
|
||||||
|
|
||||||
|
// If selected preset requests a bandwidth larger than the region span, fall back to LONG_FAST.
|
||||||
|
if (r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && (r->freqEnd - r->freqStart) < (bwKHz / 1000.0f)) {
|
||||||
|
loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
||||||
|
modemPresetToParams(loraConfig.modem_preset, regionWideLora, bwKHz, sf, cr);
|
||||||
|
}
|
||||||
|
|
||||||
|
loraConfig.bandwidth = bwKHzToCode(bwKHz);
|
||||||
|
loraConfig.spread_factor = sf;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## LoRaWAN for North America
|
* ## LoRaWAN for North America
|
||||||
|
|
||||||
@@ -474,54 +796,7 @@ void RadioInterface::applyModemConfig()
|
|||||||
bool validConfig = false; // We need to check for a valid configuration
|
bool validConfig = false; // We need to check for a valid configuration
|
||||||
while (!validConfig) {
|
while (!validConfig) {
|
||||||
if (loraConfig.use_preset) {
|
if (loraConfig.use_preset) {
|
||||||
|
modemPresetToParams(loraConfig.modem_preset, myRegion->wideLora, bw, sf, cr);
|
||||||
switch (loraConfig.modem_preset) {
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
|
|
||||||
bw = (myRegion->wideLora) ? 1625.0 : 500;
|
|
||||||
cr = 5;
|
|
||||||
sf = 7;
|
|
||||||
break;
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
|
|
||||||
bw = (myRegion->wideLora) ? 812.5 : 250;
|
|
||||||
cr = 5;
|
|
||||||
sf = 7;
|
|
||||||
break;
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
|
|
||||||
bw = (myRegion->wideLora) ? 812.5 : 250;
|
|
||||||
cr = 5;
|
|
||||||
sf = 8;
|
|
||||||
break;
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
|
|
||||||
bw = (myRegion->wideLora) ? 812.5 : 250;
|
|
||||||
cr = 5;
|
|
||||||
sf = 9;
|
|
||||||
break;
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
|
|
||||||
bw = (myRegion->wideLora) ? 812.5 : 250;
|
|
||||||
cr = 5;
|
|
||||||
sf = 10;
|
|
||||||
break;
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
|
|
||||||
bw = (myRegion->wideLora) ? 1625.0 : 500;
|
|
||||||
cr = 8;
|
|
||||||
sf = 11;
|
|
||||||
break;
|
|
||||||
default: // Config_LoRaConfig_ModemPreset_LONG_FAST is default. Gracefully use this is preset is something illegal.
|
|
||||||
bw = (myRegion->wideLora) ? 812.5 : 250;
|
|
||||||
cr = 5;
|
|
||||||
sf = 11;
|
|
||||||
break;
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
|
|
||||||
bw = (myRegion->wideLora) ? 406.25 : 125;
|
|
||||||
cr = 8;
|
|
||||||
sf = 11;
|
|
||||||
break;
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
|
|
||||||
bw = (myRegion->wideLora) ? 406.25 : 125;
|
|
||||||
cr = 8;
|
|
||||||
sf = 12;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) {
|
if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) {
|
||||||
cr = loraConfig.coding_rate;
|
cr = loraConfig.coding_rate;
|
||||||
LOG_INFO("Using custom Coding Rate %u", cr);
|
LOG_INFO("Using custom Coding Rate %u", cr);
|
||||||
@@ -529,20 +804,7 @@ void RadioInterface::applyModemConfig()
|
|||||||
} else {
|
} else {
|
||||||
sf = loraConfig.spread_factor;
|
sf = loraConfig.spread_factor;
|
||||||
cr = loraConfig.coding_rate;
|
cr = loraConfig.coding_rate;
|
||||||
bw = loraConfig.bandwidth;
|
bw = bwCodeToKHz(loraConfig.bandwidth);
|
||||||
|
|
||||||
if (bw == 31) // This parameter is not an integer
|
|
||||||
bw = 31.25;
|
|
||||||
if (bw == 62) // Fix for 62.5Khz bandwidth
|
|
||||||
bw = 62.5;
|
|
||||||
if (bw == 200)
|
|
||||||
bw = 203.125;
|
|
||||||
if (bw == 400)
|
|
||||||
bw = 406.25;
|
|
||||||
if (bw == 800)
|
|
||||||
bw = 812.5;
|
|
||||||
if (bw == 1600)
|
|
||||||
bw = 1625.0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) {
|
if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) {
|
||||||
@@ -666,18 +928,24 @@ void RadioInterface::limitPower(int8_t loraMaxPower)
|
|||||||
power = maxPower;
|
power = maxPower;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef NUM_PA_POINTS
|
#ifdef ARCH_PORTDUINO
|
||||||
if (TX_GAIN_LORA > 0 && !devicestate.owner.is_licensed) {
|
size_t num_pa_points = portduino_config.num_pa_points;
|
||||||
LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, TX_GAIN_LORA);
|
const uint16_t *tx_gain = portduino_config.tx_gain_lora;
|
||||||
power -= TX_GAIN_LORA;
|
|
||||||
}
|
|
||||||
#else
|
#else
|
||||||
if (!devicestate.owner.is_licensed) {
|
size_t num_pa_points = NUM_PA_POINTS;
|
||||||
|
const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (num_pa_points == 1) {
|
||||||
|
if (tx_gain[0] > 0 && !devicestate.owner.is_licensed) {
|
||||||
|
LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[0]);
|
||||||
|
power -= tx_gain[0];
|
||||||
|
}
|
||||||
|
} else if (!devicestate.owner.is_licensed) {
|
||||||
// we have an array of PA gain values. Find the highest power setting that works.
|
// we have an array of PA gain values. Find the highest power setting that works.
|
||||||
const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA};
|
for (int radio_dbm = 0; radio_dbm < num_pa_points; radio_dbm++) {
|
||||||
for (int radio_dbm = 0; radio_dbm < NUM_PA_POINTS; radio_dbm++) {
|
|
||||||
if (((radio_dbm + tx_gain[radio_dbm]) > power) ||
|
if (((radio_dbm + tx_gain[radio_dbm]) > power) ||
|
||||||
((radio_dbm == (NUM_PA_POINTS - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) {
|
((radio_dbm == (num_pa_points - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) {
|
||||||
// we've exceeded the power limit, or hit the max we can do
|
// we've exceeded the power limit, or hit the max we can do
|
||||||
LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[radio_dbm]);
|
LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[radio_dbm]);
|
||||||
power -= tx_gain[radio_dbm];
|
power -= tx_gain[radio_dbm];
|
||||||
@@ -685,7 +953,7 @@ void RadioInterface::limitPower(int8_t loraMaxPower)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
if (power > loraMaxPower) // Clamp power to maximum defined level
|
if (power > loraMaxPower) // Clamp power to maximum defined level
|
||||||
power = loraMaxPower;
|
power = loraMaxPower;
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user