mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-16 16:41:33 +10:00
Compare commits
143
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f18534ebd | ||
|
|
fd084abbd9 | ||
|
|
a092f6bb22 | ||
|
|
927a4e47b8 | ||
|
|
af18f061cf | ||
|
|
139e45dff8 | ||
|
|
0df8719fc8 | ||
|
|
b12cac3e00 | ||
|
|
28c4acf5c4 | ||
|
|
579a0af516 | ||
|
|
73adeee385 | ||
|
|
bb3d6d5326 | ||
|
|
7cbab4838c | ||
|
|
4ce554e09d | ||
|
|
eb145f8adc | ||
|
|
a60e7cfe62 | ||
|
|
53231ae4b1 | ||
|
|
39139cc2ea | ||
|
|
5280caf9d8 | ||
|
|
ba016fd91a | ||
|
|
e2cf401ad3 | ||
|
|
4a4b1f4a87 | ||
|
|
779e446d14 | ||
|
|
f73d18384d | ||
|
|
11bb2ee84e | ||
|
|
2361776992 | ||
|
|
a324c4af10 | ||
|
|
5df5ab2790 | ||
|
|
74ea6206d9 | ||
|
|
b238744445 | ||
|
|
94b7149958 | ||
|
|
ac611c4b62 | ||
|
|
89df5ef669 | ||
|
|
be5f0a9ade | ||
|
|
bfc3eebd54 | ||
|
|
538a5f0dfc | ||
|
|
b7db22055d | ||
|
|
b008c7a170 | ||
|
|
c8a9cdc148 | ||
|
|
0703e0e6d7 | ||
|
|
644fa5b54e | ||
|
|
f514bc230b | ||
|
|
0022148323 | ||
|
|
9d06c1bf34 | ||
|
|
1d30342c00 | ||
|
|
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 | ||
|
|
afbd9e2180 | ||
|
|
64116cd0d3 |
@@ -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
|
||||||
|
});
|
||||||
@@ -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] });
|
||||||
@@ -50,3 +50,6 @@ idf_component.yml
|
|||||||
CMakeLists.txt
|
CMakeLists.txt
|
||||||
/sdkconfig.*
|
/sdkconfig.*
|
||||||
.dummy/*
|
.dummy/*
|
||||||
|
|
||||||
|
# PYTHONPATH used by the Nix shell
|
||||||
|
.python3
|
||||||
|
|||||||
+9
-9
@@ -8,25 +8,25 @@ plugins:
|
|||||||
uri: https://github.com/trunk-io/plugins
|
uri: https://github.com/trunk-io/plugins
|
||||||
lint:
|
lint:
|
||||||
enabled:
|
enabled:
|
||||||
- checkov@3.2.497
|
- checkov@3.2.500
|
||||||
- renovate@42.81.8
|
- renovate@43.4.0
|
||||||
- prettier@3.8.0
|
- prettier@3.8.1
|
||||||
- trufflehog@3.92.4
|
- trufflehog@3.93.0
|
||||||
- yamllint@1.38.0
|
- yamllint@1.38.0
|
||||||
- bandit@1.9.2
|
- bandit@1.9.3
|
||||||
- trivy@0.68.2
|
- trivy@0.69.1
|
||||||
- taplo@0.10.0
|
- taplo@0.10.0
|
||||||
- ruff@0.14.11
|
- ruff@0.15.0
|
||||||
- isort@7.0.0
|
- isort@7.0.0
|
||||||
- markdownlint@0.47.0
|
- markdownlint@0.47.0
|
||||||
- oxipng@10.0.0
|
- oxipng@10.1.0
|
||||||
- svgo@4.0.0
|
- svgo@4.0.0
|
||||||
- actionlint@1.7.10
|
- actionlint@1.7.10
|
||||||
- flake8@7.3.0
|
- flake8@7.3.0
|
||||||
- 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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
+2
-10
@@ -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.
|
||||||
|
|||||||
+21
-14
@@ -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
|
||||||
|
|||||||
+13
-2
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
Vendored
+6
@@ -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
|
||||||
|
|||||||
Generated
+44
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -59,8 +59,14 @@ BuildRequires: pkgconfig(libbsd-overlay)
|
|||||||
|
|
||||||
Requires: systemd-udev
|
Requires: systemd-udev
|
||||||
|
|
||||||
|
# Declare that this package provides the user/group it creates in %pre
|
||||||
|
# Required for Fedora 43+ which tracks users/groups as RPM dependencies
|
||||||
|
Provides: user(%{meshtasticd_user})
|
||||||
|
Provides: group(%{meshtasticd_user})
|
||||||
|
Provides: group(spi)
|
||||||
|
|
||||||
%description
|
%description
|
||||||
Meshtastic daemon for controlling Meshtastic devices. Meshtastic is an off-grid
|
Meshtastic daemon. Meshtastic is an off-grid
|
||||||
text communication platform that uses inexpensive LoRa radios.
|
text communication platform that uses inexpensive LoRa radios.
|
||||||
|
|
||||||
%prep
|
%prep
|
||||||
@@ -151,6 +157,7 @@ fi
|
|||||||
%license LICENSE
|
%license LICENSE
|
||||||
%doc README.md
|
%doc README.md
|
||||||
%{_bindir}/meshtasticd
|
%{_bindir}/meshtasticd
|
||||||
|
%{_bindir}/meshtasticd-start.sh
|
||||||
%dir %{_localstatedir}/lib/meshtasticd
|
%dir %{_localstatedir}/lib/meshtasticd
|
||||||
%{_udevrulesdir}/99-meshtasticd-udev.rules
|
%{_udevrulesdir}/99-meshtasticd-udev.rules
|
||||||
%dir %{_sysconfdir}/meshtasticd
|
%dir %{_sysconfdir}/meshtasticd
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
+37
-3
@@ -50,12 +50,14 @@ build_flags = -Wno-missing-field-initializers
|
|||||||
-DRADIOLIB_EXCLUDE_APRS=1
|
-DRADIOLIB_EXCLUDE_APRS=1
|
||||||
-DRADIOLIB_EXCLUDE_LORAWAN=1
|
-DRADIOLIB_EXCLUDE_LORAWAN=1
|
||||||
-DMESHTASTIC_EXCLUDE_DROPZONE=1
|
-DMESHTASTIC_EXCLUDE_DROPZONE=1
|
||||||
|
-DMESHTASTIC_EXCLUDE_REPLYBOT=1
|
||||||
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
|
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
|
||||||
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
||||||
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
||||||
-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
|
||||||
@@ -65,7 +67,7 @@ monitor_speed = 115200
|
|||||||
monitor_filters = direct
|
monitor_filters = direct
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
||||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/b34c6817c25d6faabb3a8a162b5d14fb75395433.zip
|
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/21e484f409cde18d44012caef84c244eb5ca28f3.zip
|
||||||
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
||||||
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
||||||
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
||||||
@@ -119,7 +121,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/6c75195e9987b7a49563232234f2f868dd343cae.zip
|
||||||
|
|
||||||
; Common libs for environmental measurements in telemetry module
|
; Common libs for environmental measurements in telemetry module
|
||||||
[environmental_base]
|
[environmental_base]
|
||||||
@@ -143,7 +145,7 @@ lib_deps =
|
|||||||
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
|
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
|
||||||
adafruit/Adafruit INA219@1.2.3
|
adafruit/Adafruit INA219@1.2.3
|
||||||
# renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050
|
# renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050
|
||||||
adafruit/Adafruit MPU6050@2.2.6
|
adafruit/Adafruit MPU6050@2.2.8
|
||||||
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH
|
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH
|
||||||
adafruit/Adafruit LIS3DH@1.3.0
|
adafruit/Adafruit LIS3DH@1.3.0
|
||||||
# renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0
|
# renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0
|
||||||
@@ -212,3 +214,35 @@ 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
|
||||||
|
# renovate: datasource=custom.pio depName=Sensirion I2C STC3x packageName=sensirion/library/Sensirion I2C STC3x
|
||||||
|
sensirion/Sensirion I2C STC3x@1.0.1
|
||||||
|
|
||||||
|
; 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
|
||||||
|
# renovate: datasource=custom.pio depName=Sensirion I2C STC3x packageName=sensirion/library/Sensirion I2C STC3x
|
||||||
|
sensirion/Sensirion I2C STC3x@1.0.1
|
||||||
|
|||||||
+1
-1
Submodule protobufs updated: 4b9f104a18...e1a6b3a868
@@ -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
|
||||||
+2
-10
@@ -89,22 +89,14 @@ class BluetoothStatus : public Status
|
|||||||
case ConnectionState::CONNECTED:
|
case ConnectionState::CONNECTED:
|
||||||
LOG_DEBUG("BluetoothStatus CONNECTED");
|
LOG_DEBUG("BluetoothStatus CONNECTED");
|
||||||
#ifdef BLE_LED
|
#ifdef BLE_LED
|
||||||
#ifdef BLE_LED_INVERTED
|
digitalWrite(BLE_LED, LED_STATE_ON);
|
||||||
digitalWrite(BLE_LED, LOW);
|
|
||||||
#else
|
|
||||||
digitalWrite(BLE_LED, HIGH);
|
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ConnectionState::DISCONNECTED:
|
case ConnectionState::DISCONNECTED:
|
||||||
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
||||||
#ifdef BLE_LED
|
#ifdef BLE_LED
|
||||||
#ifdef BLE_LED_INVERTED
|
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||||
digitalWrite(BLE_LED, HIGH);
|
|
||||||
#else
|
|
||||||
digitalWrite(BLE_LED, LOW);
|
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
-66
@@ -1,66 +0,0 @@
|
|||||||
#include "Led.h"
|
|
||||||
#include "PowerMon.h"
|
|
||||||
#include "main.h"
|
|
||||||
#include "power.h"
|
|
||||||
|
|
||||||
GpioVirtPin ledForceOn, ledBlink;
|
|
||||||
|
|
||||||
#if defined(LED_PIN)
|
|
||||||
// Most boards have a GPIO for LED control
|
|
||||||
static GpioHwPin ledRawHwPin(LED_PIN);
|
|
||||||
#else
|
|
||||||
static GpioVirtPin ledRawHwPin; // Dummy pin for no hardware
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if LED_STATE_ON == 0
|
|
||||||
static GpioVirtPin ledHwPin;
|
|
||||||
static GpioNotTransformer ledInverter(&ledHwPin, &ledRawHwPin);
|
|
||||||
#else
|
|
||||||
static GpioPin &ledHwPin = ledRawHwPin;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(HAS_PMU)
|
|
||||||
/**
|
|
||||||
* A GPIO controlled by the PMU
|
|
||||||
*/
|
|
||||||
class GpioPmuPin : public GpioPin
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void set(bool value)
|
|
||||||
{
|
|
||||||
if (pmu_found && PMU) {
|
|
||||||
// blink the axp led
|
|
||||||
PMU->setChargingLedMode(value ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ledPmuHwPin;
|
|
||||||
|
|
||||||
// In some cases we need to drive a PMU LED and a normal LED
|
|
||||||
static GpioSplitter ledFinalPin(&ledHwPin, &ledPmuHwPin);
|
|
||||||
#else
|
|
||||||
static GpioPin &ledFinalPin = ledHwPin;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_POWERMON
|
|
||||||
/**
|
|
||||||
* We monitor changes to the LED drive output because we use that as a sanity test in our power monitor stuff.
|
|
||||||
*/
|
|
||||||
class MonitoredLedPin : public GpioPin
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void set(bool value)
|
|
||||||
{
|
|
||||||
if (powerMon) {
|
|
||||||
if (value)
|
|
||||||
powerMon->setState(meshtastic_PowerMon_State_LED_On);
|
|
||||||
else
|
|
||||||
powerMon->clearState(meshtastic_PowerMon_State_LED_On);
|
|
||||||
}
|
|
||||||
ledFinalPin.set(value);
|
|
||||||
}
|
|
||||||
} monitoredLedPin;
|
|
||||||
#else
|
|
||||||
static GpioPin &monitoredLedPin = ledFinalPin;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
static GpioBinaryTransformer ledForcer(&ledForceOn, &ledBlink, &monitoredLedPin, GpioBinaryTransformer::Or);
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#include "GpioLogic.h"
|
|
||||||
#include "configuration.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ledForceOn and ledForceOff both override the normal ledBlinker behavior (which is controlled by main)
|
|
||||||
*/
|
|
||||||
extern GpioVirtPin ledForceOn, ledBlink;
|
|
||||||
+212
-96
@@ -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,14 @@ 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)
|
// If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it.
|
||||||
return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk;
|
return false;
|
||||||
|
|
||||||
|
// 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 +489,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 +507,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 +517,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 +660,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 +669,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);
|
||||||
@@ -691,7 +692,9 @@ bool Power::setup()
|
|||||||
bool found = false;
|
bool found = false;
|
||||||
if (axpChipInit()) {
|
if (axpChipInit()) {
|
||||||
found = true;
|
found = true;
|
||||||
} else if (lipoInit()) {
|
} else if (cw2015Init()) {
|
||||||
|
found = true;
|
||||||
|
} else if (max17048Init()) {
|
||||||
found = true;
|
found = true;
|
||||||
} else if (lipoChargerInit()) {
|
} else if (lipoChargerInit()) {
|
||||||
found = true;
|
found = true;
|
||||||
@@ -723,6 +726,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 +782,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 +793,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 +820,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 +840,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 +854,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 +865,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 +944,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 +968,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 +1011,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 +1020,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 +1070,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 +1090,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 +1118,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 +1154,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 +1205,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 +1272,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 +1296,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();
|
||||||
@@ -1291,7 +1323,7 @@ bool Power::axpChipInit()
|
|||||||
/**
|
/**
|
||||||
* Wrapper class for an I2C MAX17048 Lipo battery sensor.
|
* Wrapper class for an I2C MAX17048 Lipo battery sensor.
|
||||||
*/
|
*/
|
||||||
class LipoBatteryLevel : public HasBatteryLevel
|
class MAX17048BatteryLevel : public HasBatteryLevel
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
MAX17048Singleton *max17048 = nullptr;
|
MAX17048Singleton *max17048 = nullptr;
|
||||||
@@ -1339,18 +1371,18 @@ class LipoBatteryLevel : public HasBatteryLevel
|
|||||||
virtual bool isCharging() override { return max17048->isBatteryCharging(); }
|
virtual bool isCharging() override { return max17048->isBatteryCharging(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
LipoBatteryLevel lipoLevel;
|
MAX17048BatteryLevel max17048Level;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init the Lipo battery level sensor
|
* Init the Lipo battery level sensor
|
||||||
*/
|
*/
|
||||||
bool Power::lipoInit()
|
bool Power::max17048Init()
|
||||||
{
|
{
|
||||||
bool result = lipoLevel.runOnce();
|
bool result = max17048Level.runOnce();
|
||||||
LOG_DEBUG("Power::lipoInit lipo sensor is %s", result ? "ready" : "not ready yet");
|
LOG_DEBUG("Power::max17048Init lipo sensor is %s", result ? "ready" : "not ready yet");
|
||||||
if (!result)
|
if (!result)
|
||||||
return false;
|
return false;
|
||||||
batteryLevel = &lipoLevel;
|
batteryLevel = &max17048Level;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1358,7 +1390,88 @@ bool Power::lipoInit()
|
|||||||
/**
|
/**
|
||||||
* The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
|
* The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
|
||||||
*/
|
*/
|
||||||
bool Power::lipoInit()
|
bool Power::max17048Init()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !MESHTASTIC_EXCLUDE_I2C && HAS_CW2015
|
||||||
|
|
||||||
|
class CW2015BatteryLevel : public AnalogBatteryLevel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Battery state of charge, from 0 to 100 or -1 for unknown
|
||||||
|
*/
|
||||||
|
virtual int getBatteryPercent() override
|
||||||
|
{
|
||||||
|
int data = -1;
|
||||||
|
Wire.beginTransmission(CW2015_ADDR);
|
||||||
|
Wire.write(0x04);
|
||||||
|
if (Wire.endTransmission() == 0) {
|
||||||
|
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) {
|
||||||
|
data = Wire.read();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raw voltage of the battery in millivolts, or NAN if unknown
|
||||||
|
*/
|
||||||
|
virtual uint16_t getBattVoltage() override
|
||||||
|
{
|
||||||
|
uint16_t mv = 0;
|
||||||
|
Wire.beginTransmission(CW2015_ADDR);
|
||||||
|
Wire.write(0x02);
|
||||||
|
if (Wire.endTransmission() == 0) {
|
||||||
|
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)2)) {
|
||||||
|
mv = Wire.read();
|
||||||
|
mv <<= 8;
|
||||||
|
mv |= Wire.read();
|
||||||
|
// Voltage is read in 305uV units, convert to mV
|
||||||
|
mv = mv * 305 / 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mv;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CW2015BatteryLevel cw2015Level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init the CW2015 battery level sensor
|
||||||
|
*/
|
||||||
|
bool Power::cw2015Init()
|
||||||
|
{
|
||||||
|
|
||||||
|
Wire.beginTransmission(CW2015_ADDR);
|
||||||
|
uint8_t getInfo[] = {0x0a, 0x00};
|
||||||
|
Wire.write(getInfo, 2);
|
||||||
|
Wire.endTransmission();
|
||||||
|
delay(10);
|
||||||
|
Wire.beginTransmission(CW2015_ADDR);
|
||||||
|
Wire.write(0x00);
|
||||||
|
bool result = false;
|
||||||
|
if (Wire.endTransmission() == 0) {
|
||||||
|
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) {
|
||||||
|
uint8_t data = Wire.read();
|
||||||
|
LOG_DEBUG("CW2015 init read data: 0x%x", data);
|
||||||
|
if (data == 0x73) {
|
||||||
|
result = true;
|
||||||
|
batteryLevel = &cw2015Level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
/**
|
||||||
|
* The CW2015 battery level sensor is unavailable - default to AnalogBatteryLevel
|
||||||
|
*/
|
||||||
|
bool Power::cw2015Init()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1385,8 +1498,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 +1522,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 +1558,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 +1681,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()
|
||||||
{
|
{
|
||||||
|
|||||||
+4
-4
@@ -9,13 +9,13 @@
|
|||||||
*/
|
*/
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "Default.h"
|
#include "Default.h"
|
||||||
#include "Led.h"
|
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "PowerMon.h"
|
#include "PowerMon.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "graphics/Screen.h"
|
#include "graphics/Screen.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
|
#include "modules/StatusLEDModule.h"
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
#include "target_specific.h"
|
#include "target_specific.h"
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ static void lsIdle()
|
|||||||
uint32_t sleepTime = SLEEP_TIME;
|
uint32_t sleepTime = SLEEP_TIME;
|
||||||
|
|
||||||
powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep);
|
powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep);
|
||||||
ledBlink.set(false); // Never leave led on while in light sleep
|
statusLEDModule->setPowerLED(false);
|
||||||
esp_sleep_source_t wakeCause2 = doLightSleep(sleepTime * 1000LL);
|
esp_sleep_source_t wakeCause2 = doLightSleep(sleepTime * 1000LL);
|
||||||
powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep);
|
powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep);
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ static void lsIdle()
|
|||||||
case ESP_SLEEP_WAKEUP_TIMER:
|
case ESP_SLEEP_WAKEUP_TIMER:
|
||||||
// Normal case: timer expired, we should just go back to sleep ASAP
|
// Normal case: timer expired, we should just go back to sleep ASAP
|
||||||
|
|
||||||
ledBlink.set(true); // briefly turn on led
|
statusLEDModule->setPowerLED(true);
|
||||||
wakeCause2 = doLightSleep(100); // leave led on for 1ms
|
wakeCause2 = doLightSleep(100); // leave led on for 1ms
|
||||||
|
|
||||||
secsSlept += sleepTime;
|
secsSlept += sleepTime;
|
||||||
@@ -146,7 +146,7 @@ static void lsIdle()
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Time to stop sleeping!
|
// Time to stop sleeping!
|
||||||
ledBlink.set(false);
|
statusLEDModule->setPowerLED(false);
|
||||||
LOG_INFO("Reached ls_secs, service loop()");
|
LOG_INFO("Reached ls_secs, service loop()");
|
||||||
powerFSM.trigger(EVENT_WAKE_TIMER);
|
powerFSM.trigger(EVENT_WAKE_TIMER);
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-18
@@ -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
|
||||||
@@ -229,6 +233,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define NAU7802_ADDR 0x2A
|
#define NAU7802_ADDR 0x2A
|
||||||
#define MAX30102_ADDR 0x57
|
#define MAX30102_ADDR 0x57
|
||||||
#define SCD4X_ADDR 0x62
|
#define SCD4X_ADDR 0x62
|
||||||
|
#define CW2015_ADDR 0x62
|
||||||
|
#define STC31_ADDR 0x2c // RAK12008 uses 0x2C address
|
||||||
#define MLX90614_ADDR_DEF 0x5A
|
#define MLX90614_ADDR_DEF 0x5A
|
||||||
#define CGRADSENS_ADDR 0x66
|
#define CGRADSENS_ADDR 0x66
|
||||||
#define LTR390UV_ADDR 0x53
|
#define LTR390UV_ADDR 0x53
|
||||||
@@ -237,6 +243,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define BQ27220_ADDR 0x55 // same address as TDECK_KB
|
#define BQ27220_ADDR 0x55 // same address as TDECK_KB
|
||||||
#define BQ25896_ADDR 0x6B
|
#define BQ25896_ADDR 0x6B
|
||||||
#define LTR553ALS_ADDR 0x23
|
#define LTR553ALS_ADDR 0x23
|
||||||
|
#define SEN5X_ADDR 0x69
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// ACCELEROMETER
|
// ACCELEROMETER
|
||||||
@@ -386,9 +393,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 +428,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 +453,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
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
|
|||||||
|
|
||||||
ScanI2C::FoundDevice ScanI2C::firstAQI() const
|
ScanI2C::FoundDevice ScanI2C::firstAQI() const
|
||||||
{
|
{
|
||||||
ScanI2C::DeviceType types[] = {PMSA003I, SCD4X};
|
ScanI2C::DeviceType types[] = {PMSA003I, SEN5X, SCD4X};
|
||||||
return firstOfOrNONE(2, types);
|
return firstOfOrNONE(2, types);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,10 @@ class ScanI2C
|
|||||||
BH1750,
|
BH1750,
|
||||||
DA217,
|
DA217,
|
||||||
CHSC6X,
|
CHSC6X,
|
||||||
CST226SE
|
CST226SE,
|
||||||
|
CW2015,
|
||||||
|
SEN5X,
|
||||||
|
STC31
|
||||||
} DeviceType;
|
} DeviceType;
|
||||||
|
|
||||||
// typedef uint8_t DeviceAddress;
|
// typedef uint8_t DeviceAddress;
|
||||||
|
|||||||
+141
-15
@@ -8,6 +8,7 @@
|
|||||||
#endif
|
#endif
|
||||||
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
|
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
|
||||||
#include "meshUtils.h" // vformat
|
#include "meshUtils.h" // vformat
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool in_array(uint8_t *array, int size, uint8_t lookfor)
|
bool in_array(uint8_t *array, int size, uint8_t lookfor)
|
||||||
@@ -114,6 +115,81 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// for SEN5X detection
|
||||||
|
// Note, this code needs to be called before setting the I2C bus speed
|
||||||
|
// for the screen at high speed. The speed needs to be at 100kHz, otherwise
|
||||||
|
// detection will not work
|
||||||
|
String readSEN5xProductName(TwoWire *i2cBus, uint8_t address)
|
||||||
|
{
|
||||||
|
uint8_t cmd[] = {0xD0, 0x14};
|
||||||
|
uint8_t response[48] = {0};
|
||||||
|
|
||||||
|
i2cBus->beginTransmission(address);
|
||||||
|
i2cBus->write(cmd, 2);
|
||||||
|
if (i2cBus->endTransmission() != 0)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
delay(20);
|
||||||
|
if (i2cBus->requestFrom(address, (uint8_t)48) != 48)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
for (int i = 0; i < 48 && i2cBus->available(); ++i) {
|
||||||
|
response[i] = i2cBus->read();
|
||||||
|
}
|
||||||
|
|
||||||
|
char productName[33] = {0};
|
||||||
|
int j = 0;
|
||||||
|
for (int i = 0; i < 48 && j < 32; i += 3) {
|
||||||
|
if (response[i] >= 32 && response[i] <= 126)
|
||||||
|
productName[j++] = response[i];
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (response[i + 1] >= 32 && response[i + 1] <= 126)
|
||||||
|
productName[j++] = response[i + 1];
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(productName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// for STC31 detection - probe by reading product identifier
|
||||||
|
/// Uses two-step sequence: prepareProductIdentifier (0x367c) then readProductIdentifier (0xe102)
|
||||||
|
bool probeSTC31(TwoWire *i2cBus, uint8_t address)
|
||||||
|
{
|
||||||
|
// Step 1: Send prepare product identifier command (0x367c)
|
||||||
|
uint8_t prepCmd[] = {0x36, 0x7c};
|
||||||
|
i2cBus->beginTransmission(address);
|
||||||
|
i2cBus->write(prepCmd, 2);
|
||||||
|
if (i2cBus->endTransmission() != 0)
|
||||||
|
return false;
|
||||||
|
delay(1);
|
||||||
|
|
||||||
|
// Step 2: Send read product identifier command (0xe102)
|
||||||
|
uint8_t readCmd[] = {0xe1, 0x02};
|
||||||
|
i2cBus->beginTransmission(address);
|
||||||
|
i2cBus->write(readCmd, 2);
|
||||||
|
if (i2cBus->endTransmission() != 0)
|
||||||
|
return false;
|
||||||
|
delay(10);
|
||||||
|
|
||||||
|
// Read 6 bytes: product number (4 bytes with CRC) + more data
|
||||||
|
// Product number is 32-bit: STC31 = 0x08010301
|
||||||
|
if (i2cBus->requestFrom(address, (uint8_t)6) != 6)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint8_t response[6] = {0};
|
||||||
|
for (int i = 0; i < 6 && i2cBus->available(); ++i) {
|
||||||
|
response[i] = i2cBus->read();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t productId =
|
||||||
|
((uint32_t)response[0] << 24) | ((uint32_t)response[1] << 16) | ((uint32_t)response[3] << 8) | response[4];
|
||||||
|
|
||||||
|
return productId == 0x08010301;
|
||||||
|
}
|
||||||
|
|
||||||
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
|
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
|
||||||
case ADDR: \
|
case ADDR: \
|
||||||
logFoundDevice(__VA_ARGS__); \
|
logFoundDevice(__VA_ARGS__); \
|
||||||
@@ -492,13 +568,23 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TSL25911_ADDR:
|
case TSL25911_ADDR:
|
||||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xA0 | 0x12), 1);
|
// TSL25911_ADDR (0x29) is shared by TSL2591, TSL2561, and STC31
|
||||||
if (registerValue == 0x50) {
|
// Probe for STC31 first (Sensirion CO2 sensor)
|
||||||
type = TSL2591;
|
LOG_DEBUG("Probing address 0x29 for STC31...");
|
||||||
logFoundDevice("TSL25911", (uint8_t)addr.address);
|
if (probeSTC31(i2cBus, addr.address)) {
|
||||||
|
type = STC31;
|
||||||
|
LOG_DEBUG("STC31 probe successful!");
|
||||||
|
logFoundDevice("STC31", (uint8_t)addr.address);
|
||||||
} else {
|
} else {
|
||||||
type = TSL2561;
|
LOG_DEBUG("STC31 probe failed, checking for TSL sensors...");
|
||||||
logFoundDevice("TSL2561", (uint8_t)addr.address);
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xA0 | 0x12), 1);
|
||||||
|
if (registerValue == 0x50) {
|
||||||
|
type = TSL2591;
|
||||||
|
logFoundDevice("TSL25911", (uint8_t)addr.address);
|
||||||
|
} else {
|
||||||
|
type = TSL2561;
|
||||||
|
logFoundDevice("TSL2561", (uint8_t)addr.address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -508,6 +594,18 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address);
|
||||||
SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
|
||||||
SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address);
|
||||||
|
case STC31_ADDR:
|
||||||
|
// RAK12008 uses address 0x2C for STC31
|
||||||
|
LOG_DEBUG("Probing address 0x2c for STC31...");
|
||||||
|
if (probeSTC31(i2cBus, addr.address)) {
|
||||||
|
type = STC31;
|
||||||
|
LOG_DEBUG("STC31 probe successful at 0x2c!");
|
||||||
|
logFoundDevice("STC31", (uint8_t)addr.address);
|
||||||
|
} else {
|
||||||
|
LOG_DEBUG("STC31 probe failed at 0x2c");
|
||||||
|
LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case CST328_ADDR:
|
case CST328_ADDR:
|
||||||
// Do we have the CST328 or the CST226SE
|
// Do we have the CST328 or the CST226SE
|
||||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1);
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1);
|
||||||
@@ -541,7 +639,17 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address);
|
||||||
SCAN_SIMPLE_CASE(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address);
|
case SCD4X_ADDR: {
|
||||||
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x8), 1);
|
||||||
|
if (registerValue == 0x18) {
|
||||||
|
logFoundDevice("CW2015", (uint8_t)addr.address);
|
||||||
|
type = CW2015;
|
||||||
|
} else {
|
||||||
|
logFoundDevice("SCD4X", (uint8_t)addr.address);
|
||||||
|
type = SCD4X;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address);
|
||||||
#ifdef HAS_TPS65233
|
#ifdef HAS_TPS65233
|
||||||
SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address);
|
||||||
@@ -568,8 +676,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ICM20948_ADDR: // same as BMX160_ADDR
|
case ICM20948_ADDR: // same as BMX160_ADDR and SEN5X_ADDR
|
||||||
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
|
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
|
||||||
|
// ICM20948 Register check
|
||||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
|
||||||
#ifdef HAS_ICM20948
|
#ifdef HAS_ICM20948
|
||||||
type = ICM20948;
|
type = ICM20948;
|
||||||
@@ -580,14 +689,31 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
type = ICM20948;
|
type = ICM20948;
|
||||||
logFoundDevice("ICM20948", (uint8_t)addr.address);
|
logFoundDevice("ICM20948", (uint8_t)addr.address);
|
||||||
break;
|
break;
|
||||||
} else if (addr.address == BMX160_ADDR) {
|
|
||||||
type = BMX160;
|
|
||||||
logFoundDevice("BMX160", (uint8_t)addr.address);
|
|
||||||
break;
|
|
||||||
} else {
|
} else {
|
||||||
type = MPU6050;
|
String prod = "";
|
||||||
logFoundDevice("MPU6050", (uint8_t)addr.address);
|
prod = readSEN5xProductName(i2cBus, addr.address);
|
||||||
break;
|
if (prod.startsWith("SEN55")) {
|
||||||
|
type = SEN5X;
|
||||||
|
logFoundDevice("Sensirion SEN55", addr.address);
|
||||||
|
break;
|
||||||
|
} else if (prod.startsWith("SEN54")) {
|
||||||
|
type = SEN5X;
|
||||||
|
logFoundDevice("Sensirion SEN54", addr.address);
|
||||||
|
break;
|
||||||
|
} else if (prod.startsWith("SEN50")) {
|
||||||
|
type = SEN5X;
|
||||||
|
logFoundDevice("Sensirion SEN50", addr.address);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (addr.address == BMX160_ADDR) {
|
||||||
|
type = BMX160;
|
||||||
|
logFoundDevice("BMX160", (uint8_t)addr.address);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
type = MPU6050;
|
||||||
|
logFoundDevice("MPU6050", (uint8_t)addr.address);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#include "reClockI2C.h"
|
||||||
|
#include "ScanI2CTwoWire.h"
|
||||||
|
|
||||||
|
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force)
|
||||||
|
{
|
||||||
|
|
||||||
|
uint32_t currentClock = 0;
|
||||||
|
|
||||||
|
/* See https://github.com/arduino/Arduino/issues/11457
|
||||||
|
Currently, only ESP32 can getClock()
|
||||||
|
While all cores can setClock()
|
||||||
|
https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50
|
||||||
|
https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60
|
||||||
|
https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103
|
||||||
|
For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes)
|
||||||
|
we need to reclock I2C and set it back to the previous desired speed.
|
||||||
|
Only for cases where we can know OR predefine the speed, we can do this.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO add getClock function or return a predefined clock speed per variant?
|
||||||
|
#ifdef CAN_RECLOCK_I2C
|
||||||
|
currentClock = i2cBus->getClock();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if ((currentClock != desiredClock) || force) {
|
||||||
|
LOG_DEBUG("Changing I2C clock to %u", desiredClock);
|
||||||
|
i2cBus->setClock(desiredClock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentClock;
|
||||||
|
}
|
||||||
+7
-37
@@ -1,41 +1,11 @@
|
|||||||
#ifdef CAN_RECLOCK_I2C
|
|
||||||
|
#ifndef RECLOCK_I2C_
|
||||||
|
#define RECLOCK_I2C_
|
||||||
|
|
||||||
#include "ScanI2CTwoWire.h"
|
#include "ScanI2CTwoWire.h"
|
||||||
|
#include <Wire.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus)
|
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force);
|
||||||
{
|
|
||||||
|
|
||||||
uint32_t currentClock;
|
|
||||||
|
|
||||||
/* See https://github.com/arduino/Arduino/issues/11457
|
|
||||||
Currently, only ESP32 can getClock()
|
|
||||||
While all cores can setClock()
|
|
||||||
https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50
|
|
||||||
https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60
|
|
||||||
https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103
|
|
||||||
For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes)
|
|
||||||
we need to reclock I2C and set it back to the previous desired speed.
|
|
||||||
Only for cases where we can know OR predefine the speed, we can do this.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifdef ARCH_ESP32
|
|
||||||
currentClock = i2cBus->getClock();
|
|
||||||
#elif defined(ARCH_NRF52)
|
|
||||||
// TODO add getClock function or return a predefined clock speed per variant?
|
|
||||||
return 0;
|
|
||||||
#elif defined(ARCH_RP2040)
|
|
||||||
// TODO add getClock function or return a predefined clock speed per variant
|
|
||||||
return 0;
|
|
||||||
#elif defined(ARCH_STM32WL)
|
|
||||||
// TODO add getClock function or return a predefined clock speed per variant
|
|
||||||
return 0;
|
|
||||||
#else
|
|
||||||
return 0;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (currentClock != desiredClock) {
|
|
||||||
LOG_DEBUG("Changing I2C clock to %u", desiredClock);
|
|
||||||
i2cBus->setClock(desiredClock);
|
|
||||||
}
|
|
||||||
return currentClock;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+11
-10
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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();
|
||||||
|
|||||||
@@ -221,7 +221,6 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
|
|
||||||
if (rtc_sec > 0) {
|
if (rtc_sec > 0) {
|
||||||
// === Build Time String ===
|
// === Build Time String ===
|
||||||
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
|
||||||
int hour, minute, second;
|
int hour, minute, second;
|
||||||
graphics::decomposeTime(rtc_sec, hour, minute, second);
|
graphics::decomposeTime(rtc_sec, hour, minute, second);
|
||||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
||||||
|
|||||||
@@ -429,6 +429,10 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool
|
|||||||
c = c - 'a' + 'A';
|
c = c - 'a' + 'A';
|
||||||
}
|
}
|
||||||
keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c);
|
keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c);
|
||||||
|
// Show the common "/" pairing next to "?" like on a real keyboard
|
||||||
|
if (key.type == VK_CHAR && key.character == '?') {
|
||||||
|
keyText = "?/";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int textWidth = display->getStringWidth(keyText.c_str());
|
int textWidth = display->getStringWidth(keyText.c_str());
|
||||||
@@ -518,9 +522,13 @@ char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)
|
|||||||
|
|
||||||
char c = key.character;
|
char c = key.character;
|
||||||
|
|
||||||
// Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings
|
// Long-press: letters become uppercase; for "?" provide "/" like a typical keyboard
|
||||||
if (isLongPress && c >= 'a' && c <= 'z') {
|
if (isLongPress) {
|
||||||
c = (char)(c - 'a' + 'A');
|
if (c >= 'a' && c <= 'z') {
|
||||||
|
c = (char)(c - 'a' + 'A');
|
||||||
|
} else if (c == '?') {
|
||||||
|
c = '/';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c;
|
return c;
|
||||||
|
|||||||
+171
-178
@@ -58,7 +58,7 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
menuHandler::screenMenus menuHandler::menuQueue = menu_none;
|
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
|
||||||
uint32_t menuHandler::pickedNodeNum = 0;
|
uint32_t menuHandler::pickedNodeNum = 0;
|
||||||
bool test_enabled = false;
|
bool test_enabled = false;
|
||||||
uint8_t test_count = 0;
|
uint8_t test_count = 0;
|
||||||
@@ -66,7 +66,7 @@ uint8_t test_count = 0;
|
|||||||
void menuHandler::loraMenu()
|
void menuHandler::loraMenu()
|
||||||
{
|
{
|
||||||
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "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, frequency_slot = 3, lora_picker = 4 };
|
enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 };
|
||||||
BannerOverlayOptions bannerOptions;
|
BannerOverlayOptions bannerOptions;
|
||||||
bannerOptions.message = "LoRa Actions";
|
bannerOptions.message = "LoRa Actions";
|
||||||
bannerOptions.optionsArrayPtr = optionsArray;
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
@@ -74,14 +74,14 @@ void menuHandler::loraMenu()
|
|||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == Back) {
|
if (selected == Back) {
|
||||||
// No action
|
// No action
|
||||||
} else if (selected == device_role_picker) {
|
} else if (selected == DeviceRolePicker) {
|
||||||
menuHandler::menuQueue = menuHandler::device_role_picker;
|
menuHandler::menuQueue = menuHandler::DeviceRolePicker;
|
||||||
} else if (selected == radio_preset_picker) {
|
} else if (selected == RadioPresetPicker) {
|
||||||
menuHandler::menuQueue = menuHandler::radio_preset_picker;
|
menuHandler::menuQueue = menuHandler::RadioPresetPicker;
|
||||||
} else if (selected == frequency_slot) {
|
} else if (selected == FrequencySlot) {
|
||||||
menuHandler::menuQueue = menuHandler::frequency_slot;
|
menuHandler::menuQueue = menuHandler::FrequencySlot;
|
||||||
} else if (selected == lora_picker) {
|
} else if (selected == LoraPicker) {
|
||||||
menuHandler::menuQueue = menuHandler::lora_picker;
|
menuHandler::menuQueue = menuHandler::LoraPicker;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
@@ -102,7 +102,7 @@ void menuHandler::OnboardMessage()
|
|||||||
bannerOptions.optionsArrayPtr = optionsArray;
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
bannerOptions.optionsCount = 2;
|
bannerOptions.optionsCount = 2;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
menuHandler::menuQueue = menuHandler::no_timeout_lora_picker;
|
menuHandler::menuQueue = menuHandler::NoTimeoutLoraPicker;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
};
|
};
|
||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
@@ -216,7 +216,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
|||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
void menuHandler::DeviceRolePicker()
|
void menuHandler::deviceRolePicker()
|
||||||
{
|
{
|
||||||
static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
|
static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
|
||||||
enum optionsNumbers {
|
enum optionsNumbers {
|
||||||
@@ -232,7 +232,7 @@ void menuHandler::DeviceRolePicker()
|
|||||||
bannerOptions.optionsCount = 5;
|
bannerOptions.optionsCount = 5;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == Back) {
|
if (selected == Back) {
|
||||||
menuHandler::menuQueue = menuHandler::lora_Menu;
|
menuHandler::menuQueue = menuHandler::LoraMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
return;
|
return;
|
||||||
} else if (selected == devicerole_client) {
|
} else if (selected == devicerole_client) {
|
||||||
@@ -266,52 +266,8 @@ void menuHandler::FrequencySlotPicker()
|
|||||||
|
|
||||||
// Calculate number of channels (copied from RadioInterface::applyModemConfig())
|
// Calculate number of channels (copied from RadioInterface::applyModemConfig())
|
||||||
meshtastic_Config_LoRaConfig &loraConfig = config.lora;
|
meshtastic_Config_LoRaConfig &loraConfig = config.lora;
|
||||||
double bw = loraConfig.bandwidth;
|
double bw = loraConfig.use_preset ? modemPresetToBwKHz(loraConfig.modem_preset, myRegion->wideLora)
|
||||||
if (loraConfig.use_preset) {
|
: bwCodeToKHz(loraConfig.bandwidth);
|
||||||
switch (loraConfig.modem_preset) {
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
|
|
||||||
bw = (myRegion->wideLora) ? 1625.0 : 500;
|
|
||||||
break;
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
|
|
||||||
bw = (myRegion->wideLora) ? 812.5 : 250;
|
|
||||||
break;
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
|
|
||||||
bw = (myRegion->wideLora) ? 812.5 : 250;
|
|
||||||
break;
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
|
|
||||||
bw = (myRegion->wideLora) ? 812.5 : 250;
|
|
||||||
break;
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
|
|
||||||
bw = (myRegion->wideLora) ? 812.5 : 250;
|
|
||||||
break;
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
|
|
||||||
bw = (myRegion->wideLora) ? 1625.0 : 500;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
bw = (myRegion->wideLora) ? 812.5 : 250;
|
|
||||||
break;
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
|
|
||||||
bw = (myRegion->wideLora) ? 406.25 : 125;
|
|
||||||
break;
|
|
||||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
|
|
||||||
bw = (myRegion->wideLora) ? 406.25 : 125;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bw = loraConfig.bandwidth;
|
|
||||||
if (bw == 31) // This parameter is not an integer
|
|
||||||
bw = 31.25;
|
|
||||||
if (bw == 62) // Fix for 62.5Khz bandwidth
|
|
||||||
bw = 62.5;
|
|
||||||
if (bw == 200)
|
|
||||||
bw = 203.125;
|
|
||||||
if (bw == 400)
|
|
||||||
bw = 406.25;
|
|
||||||
if (bw == 800)
|
|
||||||
bw = 812.5;
|
|
||||||
if (bw == 1600)
|
|
||||||
bw = 1625.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t numChannels = 0;
|
uint32_t numChannels = 0;
|
||||||
if (myRegion) {
|
if (myRegion) {
|
||||||
@@ -344,7 +300,7 @@ void menuHandler::FrequencySlotPicker()
|
|||||||
|
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == Back) {
|
if (selected == Back) {
|
||||||
menuHandler::menuQueue = menuHandler::lora_Menu;
|
menuHandler::menuQueue = menuHandler::LoraMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -357,7 +313,7 @@ void menuHandler::FrequencySlotPicker()
|
|||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
void menuHandler::RadioPresetPicker()
|
void menuHandler::radioPresetPicker()
|
||||||
{
|
{
|
||||||
static const RadioPresetOption presetOptions[] = {
|
static const RadioPresetOption presetOptions[] = {
|
||||||
{"Back", OptionsAction::Back},
|
{"Back", OptionsAction::Back},
|
||||||
@@ -377,7 +333,7 @@ void menuHandler::RadioPresetPicker()
|
|||||||
auto bannerOptions =
|
auto bannerOptions =
|
||||||
createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void {
|
createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void {
|
||||||
if (option.action == OptionsAction::Back) {
|
if (option.action == OptionsAction::Back) {
|
||||||
menuHandler::menuQueue = menuHandler::lora_Menu;
|
menuHandler::menuQueue = menuHandler::LoraMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -396,7 +352,7 @@ void menuHandler::RadioPresetPicker()
|
|||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
void menuHandler::TwelveHourPicker()
|
void menuHandler::twelveHourPicker()
|
||||||
{
|
{
|
||||||
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
|
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
|
||||||
enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 };
|
enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 };
|
||||||
@@ -406,7 +362,7 @@ void menuHandler::TwelveHourPicker()
|
|||||||
bannerOptions.optionsCount = 3;
|
bannerOptions.optionsCount = 3;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == Back) {
|
if (selected == Back) {
|
||||||
menuHandler::menuQueue = menuHandler::clock_menu;
|
menuHandler::menuQueue = menuHandler::ClockMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == twelve) {
|
} else if (selected == twelve) {
|
||||||
config.display.use_12h_clock = true;
|
config.display.use_12h_clock = true;
|
||||||
@@ -434,7 +390,7 @@ void menuHandler::showConfirmationBanner(const char *message, std::function<void
|
|||||||
screen->showOverlayBanner(confirmBanner);
|
screen->showOverlayBanner(confirmBanner);
|
||||||
}
|
}
|
||||||
|
|
||||||
void menuHandler::ClockFacePicker()
|
void menuHandler::clockFacePicker()
|
||||||
{
|
{
|
||||||
static const ClockFaceOption clockFaceOptions[] = {
|
static const ClockFaceOption clockFaceOptions[] = {
|
||||||
{"Back", OptionsAction::Back},
|
{"Back", OptionsAction::Back},
|
||||||
@@ -448,7 +404,7 @@ void menuHandler::ClockFacePicker()
|
|||||||
auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels,
|
auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels,
|
||||||
[](const ClockFaceOption &option, int) -> void {
|
[](const ClockFaceOption &option, int) -> void {
|
||||||
if (option.action == OptionsAction::Back) {
|
if (option.action == OptionsAction::Back) {
|
||||||
menuHandler::menuQueue = menuHandler::clock_menu;
|
menuHandler::menuQueue = menuHandler::ClockMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -500,7 +456,7 @@ void menuHandler::TZPicker()
|
|||||||
auto bannerOptions = createStaticBannerOptions(
|
auto bannerOptions = createStaticBannerOptions(
|
||||||
"Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void {
|
"Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void {
|
||||||
if (option.action == OptionsAction::Back) {
|
if (option.action == OptionsAction::Back) {
|
||||||
menuHandler::menuQueue = menuHandler::clock_menu;
|
menuHandler::menuQueue = menuHandler::ClockMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -547,13 +503,13 @@ void menuHandler::clockMenu()
|
|||||||
bannerOptions.optionsCount = 4;
|
bannerOptions.optionsCount = 4;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == Clock) {
|
if (selected == Clock) {
|
||||||
menuHandler::menuQueue = menuHandler::clock_face_picker;
|
menuHandler::menuQueue = menuHandler::ClockFacePicker;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == Time) {
|
} else if (selected == Time) {
|
||||||
menuHandler::menuQueue = menuHandler::twelve_hour_picker;
|
menuHandler::menuQueue = menuHandler::TwelveHourPicker;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == Timezone) {
|
} else if (selected == Timezone) {
|
||||||
menuHandler::menuQueue = menuHandler::TZ_picker;
|
menuHandler::menuQueue = menuHandler::TzPicker;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -616,12 +572,12 @@ void menuHandler::messageResponseMenu()
|
|||||||
LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer);
|
LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer);
|
||||||
|
|
||||||
if (selected == ViewMode) {
|
if (selected == ViewMode) {
|
||||||
menuHandler::menuQueue = menuHandler::message_viewmode_menu;
|
menuHandler::menuQueue = menuHandler::MessageViewModeMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
|
|
||||||
// Reply submenu
|
// Reply submenu
|
||||||
} else if (selected == ReplyMenu) {
|
} else if (selected == ReplyMenu) {
|
||||||
menuHandler::menuQueue = menuHandler::reply_menu;
|
menuHandler::menuQueue = menuHandler::ReplyMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
|
|
||||||
} else if (selected == MuteChannel) {
|
} else if (selected == MuteChannel) {
|
||||||
@@ -633,7 +589,7 @@ void menuHandler::messageResponseMenu()
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else if (selected == DeleteMenu) {
|
} else if (selected == DeleteMenu) {
|
||||||
menuHandler::menuQueue = menuHandler::delete_messages_menu;
|
menuHandler::menuQueue = menuHandler::DeleteMessagesMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
|
|
||||||
#ifdef HAS_I2S
|
#ifdef HAS_I2S
|
||||||
@@ -693,7 +649,7 @@ void menuHandler::replyMenu()
|
|||||||
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||||
|
|
||||||
if (selected == Back) {
|
if (selected == Back) {
|
||||||
menuHandler::menuQueue = menuHandler::message_response_menu;
|
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -781,7 +737,7 @@ void menuHandler::deleteMessagesMenu()
|
|||||||
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||||
|
|
||||||
if (selected == Back) {
|
if (selected == Back) {
|
||||||
menuHandler::menuQueue = menuHandler::message_response_menu;
|
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -945,7 +901,7 @@ void menuHandler::messageViewModeMenu()
|
|||||||
bannerOptions.bannerCallback = [=](int selected) -> void {
|
bannerOptions.bannerCallback = [=](int selected) -> void {
|
||||||
LOG_DEBUG("messageViewModeMenu: selected=%d", selected);
|
LOG_DEBUG("messageViewModeMenu: selected=%d", selected);
|
||||||
if (selected == -1) {
|
if (selected == -1) {
|
||||||
menuHandler::menuQueue = menuHandler::message_response_menu;
|
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == -2) {
|
} else if (selected == -2) {
|
||||||
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL);
|
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL);
|
||||||
@@ -1127,23 +1083,23 @@ void menuHandler::systemBaseMenu()
|
|||||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == Notifications) {
|
if (selected == Notifications) {
|
||||||
menuHandler::menuQueue = menuHandler::buzzermodemenupicker;
|
menuHandler::menuQueue = menuHandler::BuzzerModeMenuPicker;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == ScreenOptions) {
|
} else if (selected == ScreenOptions) {
|
||||||
menuHandler::menuQueue = menuHandler::screen_options_menu;
|
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == PowerMenu) {
|
} else if (selected == PowerMenu) {
|
||||||
menuHandler::menuQueue = menuHandler::power_menu;
|
menuHandler::menuQueue = menuHandler::PowerMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == Test) {
|
} else if (selected == Test) {
|
||||||
menuHandler::menuQueue = menuHandler::test_menu;
|
menuHandler::menuQueue = menuHandler::TestMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == Bluetooth) {
|
} else if (selected == Bluetooth) {
|
||||||
menuQueue = bluetooth_toggle_menu;
|
menuQueue = BluetoothToggleMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||||
} else if (selected == WiFiToggle) {
|
} else if (selected == WiFiToggle) {
|
||||||
menuQueue = wifi_toggle_menu;
|
menuQueue = WifiToggleMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
#endif
|
#endif
|
||||||
} else if (selected == Back && !test_enabled) {
|
} else if (selected == Back && !test_enabled) {
|
||||||
@@ -1221,7 +1177,7 @@ void menuHandler::favoriteBaseMenu()
|
|||||||
evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
|
evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
|
||||||
screen->handleUIFrameEvent(&evt);
|
screen->handleUIFrameEvent(&evt);
|
||||||
} else if (selected == Remove) {
|
} else if (selected == Remove) {
|
||||||
menuHandler::menuQueue = menuHandler::remove_favorite;
|
menuHandler::menuQueue = menuHandler::RemoveFavorite;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == TraceRoute) {
|
} else if (selected == TraceRoute) {
|
||||||
if (traceRouteModule) {
|
if (traceRouteModule) {
|
||||||
@@ -1282,15 +1238,15 @@ void menuHandler::positionBaseMenu()
|
|||||||
auto action = static_cast<PositionAction>(option.value);
|
auto action = static_cast<PositionAction>(option.value);
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case PositionAction::GpsToggle:
|
case PositionAction::GpsToggle:
|
||||||
menuQueue = gps_toggle_menu;
|
menuQueue = GpsToggleMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
break;
|
break;
|
||||||
case PositionAction::GpsFormat:
|
case PositionAction::GpsFormat:
|
||||||
menuQueue = gps_format_menu;
|
menuQueue = GpsFormatMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
break;
|
break;
|
||||||
case PositionAction::CompassMenu:
|
case PositionAction::CompassMenu:
|
||||||
menuQueue = compass_point_north_menu;
|
menuQueue = CompassPointNorthMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
break;
|
break;
|
||||||
case PositionAction::CompassCalibrate:
|
case PositionAction::CompassCalibrate:
|
||||||
@@ -1299,15 +1255,15 @@ void menuHandler::positionBaseMenu()
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case PositionAction::GPSSmartPosition:
|
case PositionAction::GPSSmartPosition:
|
||||||
menuQueue = gps_smart_position_menu;
|
menuQueue = GpsSmartPositionMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
break;
|
break;
|
||||||
case PositionAction::GPSUpdateInterval:
|
case PositionAction::GPSUpdateInterval:
|
||||||
menuQueue = gps_update_interval_menu;
|
menuQueue = GpsUpdateIntervalMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
break;
|
break;
|
||||||
case PositionAction::GPSPositionBroadcast:
|
case PositionAction::GPSPositionBroadcast:
|
||||||
menuQueue = gps_position_broadcast_menu;
|
menuQueue = GpsPositionBroadcastMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1347,13 +1303,13 @@ void menuHandler::nodeListMenu()
|
|||||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == NodePicker) {
|
if (selected == NodePicker) {
|
||||||
menuQueue = NodePicker_menu;
|
menuQueue = NodePickerMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == Reset) {
|
} else if (selected == Reset) {
|
||||||
menuQueue = reset_node_db_menu;
|
menuQueue = ResetNodeDbMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == NodeNameLength) {
|
} else if (selected == NodeNameLength) {
|
||||||
menuHandler::menuQueue = menuHandler::node_name_length_menu;
|
menuHandler::menuQueue = menuHandler::NodeNameLengthMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1374,12 +1330,12 @@ void menuHandler::NodePicker()
|
|||||||
menuHandler::pickedNodeNum = nodenum;
|
menuHandler::pickedNodeNum = nodenum;
|
||||||
// Keep UI favorite context in sync (used elsewhere for some node-based actions)
|
// Keep UI favorite context in sync (used elsewhere for some node-based actions)
|
||||||
graphics::UIRenderer::currentFavoriteNodeNum = nodenum;
|
graphics::UIRenderer::currentFavoriteNodeNum = nodenum;
|
||||||
menuQueue = Manage_Node_menu;
|
menuQueue = ManageNodeMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void menuHandler::ManageNodeMenu()
|
void menuHandler::manageNodeMenu()
|
||||||
{
|
{
|
||||||
// If we don't have a node selected yet, go fast exit
|
// If we don't have a node selected yet, go fast exit
|
||||||
auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
|
auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
|
||||||
@@ -1435,7 +1391,7 @@ void menuHandler::ManageNodeMenu()
|
|||||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == Back) {
|
if (selected == Back) {
|
||||||
menuQueue = node_base_menu;
|
menuQueue = NodeBaseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1527,7 +1483,7 @@ void menuHandler::nodeNameLengthMenu()
|
|||||||
auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels,
|
auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels,
|
||||||
[](const NodeNameOption &option, int) -> void {
|
[](const NodeNameOption &option, int) -> void {
|
||||||
if (option.action == OptionsAction::Back) {
|
if (option.action == OptionsAction::Back) {
|
||||||
menuQueue = node_base_menu;
|
menuQueue = NodeBaseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1542,6 +1498,7 @@ void menuHandler::nodeNameLengthMenu()
|
|||||||
|
|
||||||
config.display.use_long_node_name = option.value;
|
config.display.use_long_node_name = option.value;
|
||||||
saveUIConfig();
|
saveUIConfig();
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
LOG_INFO("Setting names to %s", option.value ? "long" : "short");
|
LOG_INFO("Setting names to %s", option.value ? "long" : "short");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1572,7 +1529,7 @@ void menuHandler::resetNodeDBMenu()
|
|||||||
nodeDB->resetNodes(1);
|
nodeDB->resetNodes(1);
|
||||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||||
} else if (selected == 0) {
|
} else if (selected == 0) {
|
||||||
menuQueue = node_base_menu;
|
menuQueue = NodeBaseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1594,7 +1551,7 @@ void menuHandler::compassNorthMenu()
|
|||||||
auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels,
|
auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels,
|
||||||
[](const CompassOption &option, int) -> void {
|
[](const CompassOption &option, int) -> void {
|
||||||
if (option.action == OptionsAction::Back) {
|
if (option.action == OptionsAction::Back) {
|
||||||
menuQueue = position_base_menu;
|
menuQueue = PositionBaseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1639,7 +1596,7 @@ void menuHandler::GPSToggleMenu()
|
|||||||
auto bannerOptions =
|
auto bannerOptions =
|
||||||
createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void {
|
createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void {
|
||||||
if (option.action == OptionsAction::Back) {
|
if (option.action == OptionsAction::Back) {
|
||||||
menuQueue = position_base_menu;
|
menuQueue = PositionBaseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1704,7 +1661,7 @@ void menuHandler::GPSFormatMenu()
|
|||||||
|
|
||||||
auto onSelection = [](const GPSFormatOption &option, int) -> void {
|
auto onSelection = [](const GPSFormatOption &option, int) -> void {
|
||||||
if (option.action == OptionsAction::Back) {
|
if (option.action == OptionsAction::Back) {
|
||||||
menuQueue = position_base_menu;
|
menuQueue = PositionBaseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1759,7 +1716,7 @@ void menuHandler::GPSSmartPositionMenu()
|
|||||||
bannerOptions.optionsCount = 3;
|
bannerOptions.optionsCount = 3;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == 0) {
|
if (selected == 0) {
|
||||||
menuQueue = position_base_menu;
|
menuQueue = PositionBaseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == 1) {
|
} else if (selected == 1) {
|
||||||
config.position.position_broadcast_smart_enabled = true;
|
config.position.position_broadcast_smart_enabled = true;
|
||||||
@@ -1788,7 +1745,7 @@ void menuHandler::GPSUpdateIntervalMenu()
|
|||||||
bannerOptions.optionsCount = 16;
|
bannerOptions.optionsCount = 16;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == 0) {
|
if (selected == 0) {
|
||||||
menuQueue = position_base_menu;
|
menuQueue = PositionBaseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == 1) {
|
} else if (selected == 1) {
|
||||||
config.position.gps_update_interval = 8;
|
config.position.gps_update_interval = 8;
|
||||||
@@ -1876,7 +1833,7 @@ void menuHandler::GPSPositionBroadcastMenu()
|
|||||||
bannerOptions.optionsCount = 17;
|
bannerOptions.optionsCount = 17;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == 0) {
|
if (selected == 0) {
|
||||||
menuQueue = position_base_menu;
|
menuQueue = PositionBaseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == 1) {
|
} else if (selected == 1) {
|
||||||
config.position.position_broadcast_secs = 60;
|
config.position.position_broadcast_secs = 60;
|
||||||
@@ -1959,7 +1916,7 @@ void menuHandler::GPSPositionBroadcastMenu()
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void menuHandler::BluetoothToggleMenu()
|
void menuHandler::bluetoothToggleMenu()
|
||||||
{
|
{
|
||||||
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
|
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
|
||||||
BannerOverlayOptions bannerOptions;
|
BannerOverlayOptions bannerOptions;
|
||||||
@@ -2087,7 +2044,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
|
|||||||
auto bannerOptions = createStaticBannerOptions(
|
auto bannerOptions = createStaticBannerOptions(
|
||||||
"Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void {
|
"Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void {
|
||||||
if (option.action == OptionsAction::Back) {
|
if (option.action == OptionsAction::Back) {
|
||||||
menuQueue = system_base_menu;
|
menuQueue = SystemBaseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2182,7 +2139,7 @@ void menuHandler::rebootMenu()
|
|||||||
messageStore.saveToFlash();
|
messageStore.saveToFlash();
|
||||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||||
} else {
|
} else {
|
||||||
menuQueue = power_menu;
|
menuQueue = PowerMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2204,7 +2161,7 @@ void menuHandler::shutdownMenu()
|
|||||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
|
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||||
inputBroker->injectInputEvent(&event);
|
inputBroker->injectInputEvent(&event);
|
||||||
} else {
|
} else {
|
||||||
menuQueue = power_menu;
|
menuQueue = PowerMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2265,14 +2222,14 @@ void menuHandler::testMenu()
|
|||||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == NumberPicker) {
|
if (selected == NumberPicker) {
|
||||||
menuQueue = number_test;
|
menuQueue = NumberTest;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == ShowChirpy) {
|
} else if (selected == ShowChirpy) {
|
||||||
screen->toggleFrameVisibility("chirpy");
|
screen->toggleFrameVisibility("chirpy");
|
||||||
screen->setFrames(Screen::FOCUS_SYSTEM);
|
screen->setFrames(Screen::FOCUS_SYSTEM);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
menuQueue = system_base_menu;
|
menuQueue = SystemBaseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2296,7 +2253,7 @@ void menuHandler::wifiBaseMenu()
|
|||||||
bannerOptions.optionsCount = 2;
|
bannerOptions.optionsCount = 2;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == Wifi_toggle) {
|
if (selected == Wifi_toggle) {
|
||||||
menuQueue = wifi_toggle_menu;
|
menuQueue = WifiToggleMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2345,9 +2302,9 @@ void menuHandler::screenOptionsMenu()
|
|||||||
hasSupportBrightness = false;
|
hasSupportBrightness = false;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits };
|
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles };
|
||||||
static const char *optionsArray[5] = {"Back"};
|
static const char *optionsArray[6] = {"Back"};
|
||||||
static int optionsEnumArray[5] = {Back};
|
static int optionsEnumArray[6] = {Back};
|
||||||
int options = 1;
|
int options = 1;
|
||||||
|
|
||||||
// Only show brightness for B&W displays
|
// Only show brightness for B&W displays
|
||||||
@@ -2369,6 +2326,9 @@ void menuHandler::screenOptionsMenu()
|
|||||||
optionsArray[options] = "Display Units";
|
optionsArray[options] = "Display Units";
|
||||||
optionsEnumArray[options++] = DisplayUnits;
|
optionsEnumArray[options++] = DisplayUnits;
|
||||||
|
|
||||||
|
optionsArray[options] = "Message Bubbles";
|
||||||
|
optionsEnumArray[options++] = MessageBubbles;
|
||||||
|
|
||||||
BannerOverlayOptions bannerOptions;
|
BannerOverlayOptions bannerOptions;
|
||||||
bannerOptions.message = "Display Options";
|
bannerOptions.message = "Display Options";
|
||||||
bannerOptions.optionsArrayPtr = optionsArray;
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
@@ -2376,10 +2336,10 @@ void menuHandler::screenOptionsMenu()
|
|||||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == Brightness) {
|
if (selected == Brightness) {
|
||||||
menuHandler::menuQueue = menuHandler::brightness_picker;
|
menuHandler::menuQueue = menuHandler::BrightnessPicker;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == ScreenColor) {
|
} else if (selected == ScreenColor) {
|
||||||
menuHandler::menuQueue = menuHandler::tftcolormenupicker;
|
menuHandler::menuQueue = menuHandler::TftColorMenuPicker;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == FrameToggles) {
|
} else if (selected == FrameToggles) {
|
||||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||||
@@ -2387,8 +2347,11 @@ void menuHandler::screenOptionsMenu()
|
|||||||
} else if (selected == DisplayUnits) {
|
} else if (selected == DisplayUnits) {
|
||||||
menuHandler::menuQueue = menuHandler::DisplayUnits;
|
menuHandler::menuQueue = menuHandler::DisplayUnits;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
|
} else if (selected == MessageBubbles) {
|
||||||
|
menuHandler::menuQueue = menuHandler::MessageBubblesMenu;
|
||||||
|
screen->runNow();
|
||||||
} else {
|
} else {
|
||||||
menuQueue = system_base_menu;
|
menuQueue = SystemBaseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2424,16 +2387,16 @@ void menuHandler::powerMenu()
|
|||||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == Reboot) {
|
if (selected == Reboot) {
|
||||||
menuHandler::menuQueue = menuHandler::reboot_menu;
|
menuHandler::menuQueue = menuHandler::RebootMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == Shutdown) {
|
} else if (selected == Shutdown) {
|
||||||
menuHandler::menuQueue = menuHandler::shutdown_menu;
|
menuHandler::menuQueue = menuHandler::ShutdownMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else if (selected == MUI) {
|
} else if (selected == MUI) {
|
||||||
menuHandler::menuQueue = menuHandler::mui_picker;
|
menuHandler::menuQueue = menuHandler::MuiPicker;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
} else {
|
} else {
|
||||||
menuQueue = system_base_menu;
|
menuQueue = SystemBaseMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2471,7 +2434,7 @@ void menuHandler::keyVerificationFinalPrompt()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void menuHandler::FrameToggles_menu()
|
void menuHandler::frameTogglesMenu()
|
||||||
{
|
{
|
||||||
enum optionsNumbers {
|
enum optionsNumbers {
|
||||||
Finish,
|
Finish,
|
||||||
@@ -2615,7 +2578,7 @@ void menuHandler::FrameToggles_menu()
|
|||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
void menuHandler::DisplayUnits_menu()
|
void menuHandler::displayUnitsMenu()
|
||||||
{
|
{
|
||||||
enum optionsNumbers { Back, MetricUnits, ImperialUnits };
|
enum optionsNumbers { Back, MetricUnits, ImperialUnits };
|
||||||
|
|
||||||
@@ -2636,7 +2599,34 @@ void menuHandler::DisplayUnits_menu()
|
|||||||
config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL;
|
config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL;
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
} else {
|
} else {
|
||||||
menuHandler::menuQueue = menuHandler::screen_options_menu;
|
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
|
||||||
|
screen->runNow();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
screen->showOverlayBanner(bannerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::messageBubblesMenu()
|
||||||
|
{
|
||||||
|
enum optionsNumbers { Back, ShowBubbles, HideBubbles };
|
||||||
|
|
||||||
|
static const char *optionsArray[] = {"Back", "Show Bubbles", "Hide Bubbles"};
|
||||||
|
BannerOverlayOptions bannerOptions;
|
||||||
|
bannerOptions.message = "Message Bubbles";
|
||||||
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
|
bannerOptions.optionsCount = 3;
|
||||||
|
bannerOptions.InitialSelected = config.display.enable_message_bubbles ? 1 : 2;
|
||||||
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
|
if (selected == ShowBubbles) {
|
||||||
|
config.display.enable_message_bubbles = true;
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
LOG_INFO("Message bubbles enabled");
|
||||||
|
} else if (selected == HideBubbles) {
|
||||||
|
config.display.enable_message_bubbles = false;
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
LOG_INFO("Message bubbles disabled");
|
||||||
|
} else {
|
||||||
|
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2645,153 +2635,156 @@ void menuHandler::DisplayUnits_menu()
|
|||||||
|
|
||||||
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||||
{
|
{
|
||||||
if (menuQueue != menu_none)
|
if (menuQueue != MenuNone)
|
||||||
test_count = 0;
|
test_count = 0;
|
||||||
switch (menuQueue) {
|
switch (menuQueue) {
|
||||||
case menu_none:
|
case MenuNone:
|
||||||
break;
|
break;
|
||||||
case lora_Menu:
|
case LoraMenu:
|
||||||
loraMenu();
|
loraMenu();
|
||||||
break;
|
break;
|
||||||
case lora_picker:
|
case LoraPicker:
|
||||||
LoraRegionPicker();
|
LoraRegionPicker();
|
||||||
break;
|
break;
|
||||||
case device_role_picker:
|
case DeviceRolePicker:
|
||||||
DeviceRolePicker();
|
deviceRolePicker();
|
||||||
break;
|
break;
|
||||||
case radio_preset_picker:
|
case RadioPresetPicker:
|
||||||
RadioPresetPicker();
|
radioPresetPicker();
|
||||||
break;
|
break;
|
||||||
case frequency_slot:
|
case FrequencySlot:
|
||||||
FrequencySlotPicker();
|
FrequencySlotPicker();
|
||||||
break;
|
break;
|
||||||
case no_timeout_lora_picker:
|
case NoTimeoutLoraPicker:
|
||||||
LoraRegionPicker(0);
|
LoraRegionPicker(0);
|
||||||
break;
|
break;
|
||||||
case TZ_picker:
|
case TzPicker:
|
||||||
TZPicker();
|
TZPicker();
|
||||||
break;
|
break;
|
||||||
case twelve_hour_picker:
|
case TwelveHourPicker:
|
||||||
TwelveHourPicker();
|
twelveHourPicker();
|
||||||
break;
|
break;
|
||||||
case clock_face_picker:
|
case ClockFacePicker:
|
||||||
ClockFacePicker();
|
clockFacePicker();
|
||||||
break;
|
break;
|
||||||
case clock_menu:
|
case ClockMenu:
|
||||||
clockMenu();
|
clockMenu();
|
||||||
break;
|
break;
|
||||||
case system_base_menu:
|
case SystemBaseMenu:
|
||||||
systemBaseMenu();
|
systemBaseMenu();
|
||||||
break;
|
break;
|
||||||
case position_base_menu:
|
case PositionBaseMenu:
|
||||||
positionBaseMenu();
|
positionBaseMenu();
|
||||||
break;
|
break;
|
||||||
case node_base_menu:
|
case NodeBaseMenu:
|
||||||
nodeListMenu();
|
nodeListMenu();
|
||||||
break;
|
break;
|
||||||
#if !MESHTASTIC_EXCLUDE_GPS
|
#if !MESHTASTIC_EXCLUDE_GPS
|
||||||
case gps_toggle_menu:
|
case GpsToggleMenu:
|
||||||
GPSToggleMenu();
|
GPSToggleMenu();
|
||||||
break;
|
break;
|
||||||
case gps_format_menu:
|
case GpsFormatMenu:
|
||||||
GPSFormatMenu();
|
GPSFormatMenu();
|
||||||
break;
|
break;
|
||||||
case gps_smart_position_menu:
|
case GpsSmartPositionMenu:
|
||||||
GPSSmartPositionMenu();
|
GPSSmartPositionMenu();
|
||||||
break;
|
break;
|
||||||
case gps_update_interval_menu:
|
case GpsUpdateIntervalMenu:
|
||||||
GPSUpdateIntervalMenu();
|
GPSUpdateIntervalMenu();
|
||||||
break;
|
break;
|
||||||
case gps_position_broadcast_menu:
|
case GpsPositionBroadcastMenu:
|
||||||
GPSPositionBroadcastMenu();
|
GPSPositionBroadcastMenu();
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
case compass_point_north_menu:
|
case CompassPointNorthMenu:
|
||||||
compassNorthMenu();
|
compassNorthMenu();
|
||||||
break;
|
break;
|
||||||
case reset_node_db_menu:
|
case ResetNodeDbMenu:
|
||||||
resetNodeDBMenu();
|
resetNodeDBMenu();
|
||||||
break;
|
break;
|
||||||
case buzzermodemenupicker:
|
case BuzzerModeMenuPicker:
|
||||||
BuzzerModeMenu();
|
BuzzerModeMenu();
|
||||||
break;
|
break;
|
||||||
case mui_picker:
|
case MuiPicker:
|
||||||
switchToMUIMenu();
|
switchToMUIMenu();
|
||||||
break;
|
break;
|
||||||
case tftcolormenupicker:
|
case TftColorMenuPicker:
|
||||||
TFTColorPickerMenu(display);
|
TFTColorPickerMenu(display);
|
||||||
break;
|
break;
|
||||||
case brightness_picker:
|
case BrightnessPicker:
|
||||||
BrightnessPickerMenu();
|
BrightnessPickerMenu();
|
||||||
break;
|
break;
|
||||||
case node_name_length_menu:
|
case NodeNameLengthMenu:
|
||||||
nodeNameLengthMenu();
|
nodeNameLengthMenu();
|
||||||
break;
|
break;
|
||||||
case reboot_menu:
|
case RebootMenu:
|
||||||
rebootMenu();
|
rebootMenu();
|
||||||
break;
|
break;
|
||||||
case shutdown_menu:
|
case ShutdownMenu:
|
||||||
shutdownMenu();
|
shutdownMenu();
|
||||||
break;
|
break;
|
||||||
case NodePicker_menu:
|
case NodePickerMenu:
|
||||||
NodePicker();
|
NodePicker();
|
||||||
break;
|
break;
|
||||||
case Manage_Node_menu:
|
case ManageNodeMenu:
|
||||||
ManageNodeMenu();
|
manageNodeMenu();
|
||||||
break;
|
break;
|
||||||
case remove_favorite:
|
case RemoveFavorite:
|
||||||
removeFavoriteMenu();
|
removeFavoriteMenu();
|
||||||
break;
|
break;
|
||||||
case trace_route_menu:
|
case TraceRouteMenu:
|
||||||
traceRouteMenu();
|
traceRouteMenu();
|
||||||
break;
|
break;
|
||||||
case test_menu:
|
case TestMenu:
|
||||||
testMenu();
|
testMenu();
|
||||||
break;
|
break;
|
||||||
case number_test:
|
case NumberTest:
|
||||||
numberTest();
|
numberTest();
|
||||||
break;
|
break;
|
||||||
case wifi_toggle_menu:
|
case WifiToggleMenu:
|
||||||
wifiToggleMenu();
|
wifiToggleMenu();
|
||||||
break;
|
break;
|
||||||
case key_verification_init:
|
case KeyVerificationInit:
|
||||||
keyVerificationInitMenu();
|
keyVerificationInitMenu();
|
||||||
break;
|
break;
|
||||||
case key_verification_final_prompt:
|
case KeyVerificationFinalPrompt:
|
||||||
keyVerificationFinalPrompt();
|
keyVerificationFinalPrompt();
|
||||||
break;
|
break;
|
||||||
case bluetooth_toggle_menu:
|
case BluetoothToggleMenu:
|
||||||
BluetoothToggleMenu();
|
bluetoothToggleMenu();
|
||||||
break;
|
break;
|
||||||
case screen_options_menu:
|
case ScreenOptionsMenu:
|
||||||
screenOptionsMenu();
|
screenOptionsMenu();
|
||||||
break;
|
break;
|
||||||
case power_menu:
|
case PowerMenu:
|
||||||
powerMenu();
|
powerMenu();
|
||||||
break;
|
break;
|
||||||
case FrameToggles:
|
case FrameToggles:
|
||||||
FrameToggles_menu();
|
frameTogglesMenu();
|
||||||
break;
|
break;
|
||||||
case DisplayUnits:
|
case DisplayUnits:
|
||||||
DisplayUnits_menu();
|
displayUnitsMenu();
|
||||||
break;
|
break;
|
||||||
case throttle_message:
|
case ThrottleMessage:
|
||||||
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
|
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
|
||||||
break;
|
break;
|
||||||
case message_response_menu:
|
case MessageResponseMenu:
|
||||||
messageResponseMenu();
|
messageResponseMenu();
|
||||||
break;
|
break;
|
||||||
case reply_menu:
|
case ReplyMenu:
|
||||||
replyMenu();
|
replyMenu();
|
||||||
break;
|
break;
|
||||||
case delete_messages_menu:
|
case DeleteMessagesMenu:
|
||||||
deleteMessagesMenu();
|
deleteMessagesMenu();
|
||||||
break;
|
break;
|
||||||
case message_viewmode_menu:
|
case MessageViewModeMenu:
|
||||||
messageViewModeMenu();
|
messageViewModeMenu();
|
||||||
break;
|
break;
|
||||||
|
case MessageBubblesMenu:
|
||||||
|
messageBubblesMenu();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
menuQueue = menu_none;
|
menuQueue = MenuNone;
|
||||||
}
|
}
|
||||||
|
|
||||||
void menuHandler::saveUIConfig()
|
void menuHandler::saveUIConfig()
|
||||||
|
|||||||
@@ -8,53 +8,54 @@ class menuHandler
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
enum screenMenus {
|
enum screenMenus {
|
||||||
menu_none,
|
MenuNone,
|
||||||
lora_Menu,
|
LoraMenu,
|
||||||
lora_picker,
|
LoraPicker,
|
||||||
device_role_picker,
|
DeviceRolePicker,
|
||||||
radio_preset_picker,
|
RadioPresetPicker,
|
||||||
frequency_slot,
|
FrequencySlot,
|
||||||
no_timeout_lora_picker,
|
NoTimeoutLoraPicker,
|
||||||
TZ_picker,
|
TzPicker,
|
||||||
twelve_hour_picker,
|
TwelveHourPicker,
|
||||||
clock_face_picker,
|
ClockFacePicker,
|
||||||
clock_menu,
|
ClockMenu,
|
||||||
position_base_menu,
|
PositionBaseMenu,
|
||||||
node_base_menu,
|
NodeBaseMenu,
|
||||||
gps_toggle_menu,
|
GpsToggleMenu,
|
||||||
gps_format_menu,
|
GpsFormatMenu,
|
||||||
gps_smart_position_menu,
|
GpsSmartPositionMenu,
|
||||||
gps_update_interval_menu,
|
GpsUpdateIntervalMenu,
|
||||||
gps_position_broadcast_menu,
|
GpsPositionBroadcastMenu,
|
||||||
compass_point_north_menu,
|
CompassPointNorthMenu,
|
||||||
reset_node_db_menu,
|
ResetNodeDbMenu,
|
||||||
buzzermodemenupicker,
|
BuzzerModeMenuPicker,
|
||||||
mui_picker,
|
MuiPicker,
|
||||||
tftcolormenupicker,
|
TftColorMenuPicker,
|
||||||
brightness_picker,
|
BrightnessPicker,
|
||||||
reboot_menu,
|
RebootMenu,
|
||||||
shutdown_menu,
|
ShutdownMenu,
|
||||||
NodePicker_menu,
|
NodePickerMenu,
|
||||||
Manage_Node_menu,
|
ManageNodeMenu,
|
||||||
remove_favorite,
|
RemoveFavorite,
|
||||||
test_menu,
|
TestMenu,
|
||||||
number_test,
|
NumberTest,
|
||||||
wifi_toggle_menu,
|
WifiToggleMenu,
|
||||||
bluetooth_toggle_menu,
|
BluetoothToggleMenu,
|
||||||
screen_options_menu,
|
ScreenOptionsMenu,
|
||||||
power_menu,
|
PowerMenu,
|
||||||
system_base_menu,
|
SystemBaseMenu,
|
||||||
key_verification_init,
|
KeyVerificationInit,
|
||||||
key_verification_final_prompt,
|
KeyVerificationFinalPrompt,
|
||||||
trace_route_menu,
|
TraceRouteMenu,
|
||||||
throttle_message,
|
ThrottleMessage,
|
||||||
message_response_menu,
|
MessageResponseMenu,
|
||||||
message_viewmode_menu,
|
MessageViewModeMenu,
|
||||||
reply_menu,
|
ReplyMenu,
|
||||||
delete_messages_menu,
|
DeleteMessagesMenu,
|
||||||
node_name_length_menu,
|
NodeNameLengthMenu,
|
||||||
FrameToggles,
|
FrameToggles,
|
||||||
DisplayUnits
|
DisplayUnits,
|
||||||
|
MessageBubblesMenu
|
||||||
};
|
};
|
||||||
static screenMenus menuQueue;
|
static screenMenus menuQueue;
|
||||||
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
|
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
|
||||||
@@ -62,15 +63,15 @@ class menuHandler
|
|||||||
static void OnboardMessage();
|
static void OnboardMessage();
|
||||||
static void LoraRegionPicker(uint32_t duration = 30000);
|
static void LoraRegionPicker(uint32_t duration = 30000);
|
||||||
static void loraMenu();
|
static void loraMenu();
|
||||||
static void DeviceRolePicker();
|
static void deviceRolePicker();
|
||||||
static void RadioPresetPicker();
|
static void radioPresetPicker();
|
||||||
static void FrequencySlotPicker();
|
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();
|
||||||
static void TZPicker();
|
static void TZPicker();
|
||||||
static void TwelveHourPicker();
|
static void twelveHourPicker();
|
||||||
static void ClockFacePicker();
|
static void clockFacePicker();
|
||||||
static void messageResponseMenu();
|
static void messageResponseMenu();
|
||||||
static void messageViewModeMenu();
|
static void messageViewModeMenu();
|
||||||
static void replyMenu();
|
static void replyMenu();
|
||||||
@@ -95,7 +96,7 @@ class menuHandler
|
|||||||
static void rebootMenu();
|
static void rebootMenu();
|
||||||
static void shutdownMenu();
|
static void shutdownMenu();
|
||||||
static void NodePicker();
|
static void NodePicker();
|
||||||
static void ManageNodeMenu();
|
static void manageNodeMenu();
|
||||||
static void addFavoriteMenu();
|
static void addFavoriteMenu();
|
||||||
static void removeFavoriteMenu();
|
static void removeFavoriteMenu();
|
||||||
static void traceRouteMenu();
|
static void traceRouteMenu();
|
||||||
@@ -106,15 +107,16 @@ class menuHandler
|
|||||||
static void screenOptionsMenu();
|
static void screenOptionsMenu();
|
||||||
static void powerMenu();
|
static void powerMenu();
|
||||||
static void nodeNameLengthMenu();
|
static void nodeNameLengthMenu();
|
||||||
static void FrameToggles_menu();
|
static void frameTogglesMenu();
|
||||||
static void DisplayUnits_menu();
|
static void displayUnitsMenu();
|
||||||
|
static void messageBubblesMenu();
|
||||||
static void textMessageMenu();
|
static void textMessageMenu();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void saveUIConfig();
|
static void saveUIConfig();
|
||||||
static void keyVerificationInitMenu();
|
static void keyVerificationInitMenu();
|
||||||
static void keyVerificationFinalPrompt();
|
static void keyVerificationFinalPrompt();
|
||||||
static void BluetoothToggleMenu();
|
static void bluetoothToggleMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Generic Menu Options designations */
|
/* Generic Menu Options designations */
|
||||||
@@ -140,7 +142,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,18 @@ 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;
|
// Check if bubbles are enabled
|
||||||
|
const bool showBubbles = config.display.enable_message_bubbles;
|
||||||
|
const int textIndent = showBubbles ? (BUBBLE_PAD_X + BUBBLE_TEXT_INDENT) : LEFT_MARGIN;
|
||||||
|
|
||||||
|
// Derived widths
|
||||||
|
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (showBubbles ? (BUBBLE_PAD_X * 2) : 0);
|
||||||
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 +595,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 +683,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 +736,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 +785,124 @@ 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 (only if enabled)
|
||||||
|
if (showBubbles) {
|
||||||
|
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 + (textIndent * 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);
|
||||||
|
|
||||||
|
// Draw rounded rectangle bubble
|
||||||
|
if (bubbleW > BUBBLE_RADIUS * 2 && bubbleH > BUBBLE_RADIUS * 2) {
|
||||||
|
const int r = BUBBLE_RADIUS;
|
||||||
|
const int bx = bubbleX;
|
||||||
|
const int by = topY;
|
||||||
|
const int bw = bubbleW;
|
||||||
|
const int bh = bubbleH;
|
||||||
|
|
||||||
|
// Draw the 4 corner arcs using drawCircleQuads
|
||||||
|
display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left
|
||||||
|
display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right
|
||||||
|
display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left
|
||||||
|
display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right
|
||||||
|
|
||||||
|
// Draw the 4 edges between corners
|
||||||
|
display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge
|
||||||
|
display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge
|
||||||
|
display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge
|
||||||
|
display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge
|
||||||
|
} else if (bubbleW > 1 && bubbleH > 1) {
|
||||||
|
// Fallback to simple rectangle for very small bubbles
|
||||||
|
display->drawRect(bubbleX, topY, bubbleW, bubbleH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // end if (showBubbles)
|
||||||
|
|
||||||
// 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 +911,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 - (showBubbles ? textIndent : 0);
|
||||||
if (headerX < LEFT_MARGIN)
|
if (headerX < LEFT_MARGIN)
|
||||||
headerX = LEFT_MARGIN;
|
headerX = LEFT_MARGIN;
|
||||||
} else {
|
} else {
|
||||||
headerX = x;
|
headerX = x + textIndent;
|
||||||
}
|
}
|
||||||
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 +950,27 @@ 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 - (showBubbles ? textIndent : 0);
|
||||||
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 + textIndent, 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 +1033,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 +1042,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 +1064,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 +1112,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';
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#include "graphics/niche/InkHUD/Tile.h"
|
||||||
|
#include <cstdint>
|
||||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
#include "./Applet.h"
|
#include "./Applet.h"
|
||||||
@@ -55,7 +57,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 +67,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 +118,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 +150,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -778,6 +787,16 @@ void InkHUD::Applet::drawHeader(std::string text)
|
|||||||
drawPixel(x, 0, BLACK);
|
drawPixel(x, 0, BLACK);
|
||||||
drawPixel(x, headerDivY, BLACK); // Dotted 50%
|
drawPixel(x, headerDivY, BLACK); // Dotted 50%
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dither near battery
|
||||||
|
if (settings->optionalFeatures.batteryIcon) {
|
||||||
|
constexpr uint16_t ditherSizePx = 4;
|
||||||
|
Tile *batteryTile = ((Applet *)inkhud->getSystemApplet("BatteryIcon"))->getTile();
|
||||||
|
const uint16_t batteryTileLeft = batteryTile->getLeft();
|
||||||
|
const uint16_t batteryTileTop = batteryTile->getTop();
|
||||||
|
const uint16_t batteryTileHeight = batteryTile->getHeight();
|
||||||
|
hatchRegion(batteryTileLeft - ditherSizePx, batteryTileTop, ditherSizePx, batteryTileHeight, 2, WHITE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the height of the standard applet header
|
// Get the height of the standard applet header
|
||||||
|
|||||||
@@ -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,39 +46,29 @@ 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
|
// Clear the region beneath the tile, including the border
|
||||||
// - size of icon controlled by size of tile
|
|
||||||
int16_t l = 0;
|
|
||||||
int16_t t = 0;
|
|
||||||
uint16_t w = width();
|
|
||||||
int16_t h = height();
|
|
||||||
|
|
||||||
// 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
|
||||||
// We do need to do this with the battery though, as it is an "overlay"
|
// We do need to do this with the battery though, as it is an "overlay"
|
||||||
fillRect(l, t, w, h, WHITE);
|
fillRect(0, 0, width(), height(), WHITE);
|
||||||
|
|
||||||
// Vertical centerline
|
|
||||||
const int16_t m = t + (h / 2);
|
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// Draw battery outline
|
// Draw battery outline
|
||||||
// =====================
|
// =====================
|
||||||
|
|
||||||
// Positive terminal "bump"
|
// Positive terminal "bump"
|
||||||
const int16_t &bumpL = l;
|
|
||||||
const uint16_t bumpH = h / 2;
|
|
||||||
const int16_t bumpT = m - (bumpH / 2);
|
|
||||||
constexpr uint16_t bumpW = 2;
|
constexpr uint16_t bumpW = 2;
|
||||||
|
const int16_t &bumpL = 1;
|
||||||
|
const uint16_t bumpH = (height() - 2) / 2;
|
||||||
|
const int16_t bumpT = (1 + ((height() - 2) / 2)) - (bumpH / 2);
|
||||||
fillRect(bumpL, bumpT, bumpW, bumpH, BLACK);
|
fillRect(bumpL, bumpT, bumpW, bumpH, BLACK);
|
||||||
|
|
||||||
// Main body of battery
|
// Main body of battery
|
||||||
const int16_t bodyL = bumpL + bumpW;
|
const int16_t bodyL = 1 + bumpW;
|
||||||
const int16_t &bodyT = t;
|
const int16_t &bodyT = 1;
|
||||||
const int16_t &bodyH = h;
|
const int16_t &bodyH = height() - 2; // Handle top/bottom padding
|
||||||
const int16_t bodyW = w - bumpW;
|
const int16_t bodyW = (width() - 1) - bumpW; // Handle 1px left pad
|
||||||
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
|
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
|
||||||
|
|
||||||
// Erase join between bump and body
|
// Erase join between bump and body
|
||||||
@@ -87,12 +79,13 @@ void InkHUD::BatteryIconApplet::onRender()
|
|||||||
// ===================
|
// ===================
|
||||||
|
|
||||||
constexpr int16_t slicePad = 2;
|
constexpr int16_t slicePad = 2;
|
||||||
const int16_t sliceL = bodyL + slicePad;
|
int16_t sliceL = bodyL + slicePad;
|
||||||
const int16_t sliceT = bodyT + slicePad;
|
const int16_t sliceT = bodyT + slicePad;
|
||||||
const uint16_t sliceH = bodyH - (slicePad * 2);
|
const uint16_t sliceH = bodyH - (slicePad * 2);
|
||||||
uint16_t sliceW = bodyW - (slicePad * 2);
|
uint16_t sliceW = bodyW - (slicePad * 2);
|
||||||
|
|
||||||
sliceW = (sliceW * socRounded) / 100; // Apply percentage
|
sliceW = (sliceW * socRounded) / 100; // Apply percentage
|
||||||
|
sliceL += ((bodyW - (slicePad * 2)) - sliceW); // Shift slice to the battery's negative terminal, correcting drain direction
|
||||||
|
|
||||||
hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK);
|
hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK);
|
||||||
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
|
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
|
||||||
|
|||||||
@@ -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,19 +497,23 @@ 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;
|
||||||
const uint16_t batteryIconWidth = batteryIconHeight * 1.8;
|
const uint16_t batteryIconWidth = batteryIconHeight * 1.8;
|
||||||
inkhud->getSystemApplet("BatteryIcon")
|
inkhud->getSystemApplet("BatteryIcon")
|
||||||
->getTile()
|
->getTile()
|
||||||
->setRegion(inkhud->width() - batteryIconWidth, // x
|
->setRegion(inkhud->width() - batteryIconWidth - 1, // x
|
||||||
2, // y
|
1, // y
|
||||||
batteryIconWidth, // width
|
batteryIconWidth + 1, // width
|
||||||
batteryIconHeight); // height
|
batteryIconHeight + 2); // height
|
||||||
|
|
||||||
// Note: the tiles of placeholder and menu applets are manipulated specially
|
// Note: the tiles of placeholder and menu applets are manipulated specially
|
||||||
// - menuApplet borrows user tiles
|
// - menuApplet borrows user tiles
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user