mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-18 17:31:42 +10:00
Compare commits
120
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40ef34aa23 | ||
|
|
c8ff02cc35 | ||
|
|
d770ef27c7 | ||
|
|
71edf2fb48 | ||
|
|
c79e1925d9 | ||
|
|
3d649d518f | ||
|
|
947f8176dc | ||
|
|
d5bde83ff5 | ||
|
|
c3321771ef | ||
|
|
b6e5534a8c | ||
|
|
f10d786d82 | ||
|
|
1a7f560372 | ||
|
|
38c7ad0ed6 | ||
|
|
bcc24705a5 | ||
|
|
8137200cb8 | ||
|
|
bfbce2e314 | ||
|
|
f86291546d | ||
|
|
640dd5f7d7 | ||
|
+1 |
15297cb56f | ||
|
|
6d299eac67 | ||
|
|
31fe15bb73 | ||
|
|
97983d8014 | ||
|
|
c366e53326 | ||
|
|
0adcadb1ce | ||
|
|
21d5a34fec | ||
|
|
648148af8a | ||
|
|
5b03b1fe52 | ||
|
|
f96a8593f2 | ||
|
|
4954723f97 | ||
|
|
e047397642 | ||
|
|
75f3d123f3 | ||
|
|
6b73ac3850 | ||
|
|
44941b79cd | ||
|
|
65adfa894f | ||
|
|
6df0449408 | ||
|
|
a092f6bb22 | ||
|
|
927a4e47b8 | ||
|
|
df0ee74b30 | ||
|
|
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 |
@@ -20,7 +20,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
|
|||||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||||
cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \
|
cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \
|
||||||
libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \
|
libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \
|
||||||
libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev && \
|
libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev libsdl2-dev && \
|
||||||
apt-get clean && rm -rf /var/lib/apt/lists/* && \
|
apt-get clean && rm -rf /var/lib/apt/lists/* && \
|
||||||
pip install --no-cache-dir -U \
|
pip install --no-cache-dir -U \
|
||||||
platformio==6.1.16 \
|
platformio==6.1.16 \
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+7
-7
@@ -8,18 +8,18 @@ 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.501
|
||||||
- renovate@42.84.2
|
- renovate@43.10.3
|
||||||
- prettier@3.8.0
|
- prettier@3.8.1
|
||||||
- trufflehog@3.92.5
|
- trufflehog@3.93.3
|
||||||
- yamllint@1.38.0
|
- yamllint@1.38.0
|
||||||
- bandit@1.9.3
|
- bandit@1.9.3
|
||||||
- trivy@0.68.2
|
- trivy@0.69.1
|
||||||
- taplo@0.10.0
|
- taplo@0.10.0
|
||||||
- ruff@0.14.13
|
- ruff@0.15.1
|
||||||
- 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
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
|
|||||||
curl wget g++ zip git ca-certificates pkg-config \
|
curl wget g++ zip git ca-certificates pkg-config \
|
||||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
||||||
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev \
|
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||||
&& pip install --no-cache-dir -U platformio \
|
&& pip install --no-cache-dir -U platformio \
|
||||||
&& mkdir /tmp/firmware
|
&& mkdir /tmp/firmware
|
||||||
@@ -53,7 +53,7 @@ USER root
|
|||||||
RUN apt-get update && apt-get --no-install-recommends -y install \
|
RUN apt-get update && apt-get --no-install-recommends -y install \
|
||||||
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
|
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
|
||||||
liborcania2.3 libulfius2.7t64 libssl3t64 \
|
liborcania2.3 libulfius2.7t64 libssl3t64 \
|
||||||
libx11-6 libinput10 libxkbcommon-x11-0 \
|
libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||||
&& mkdir -p /var/lib/meshtasticd \
|
&& mkdir -p /var/lib/meshtasticd \
|
||||||
&& mkdir -p /etc/meshtasticd/config.d \
|
&& mkdir -p /etc/meshtasticd/config.d \
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@ RUN apk --no-cache add \
|
|||||||
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
|
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
|
||||||
libgpiod-dev yaml-cpp-dev bluez-dev \
|
libgpiod-dev yaml-cpp-dev bluez-dev \
|
||||||
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
|
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
|
||||||
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev \
|
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
|
||||||
&& rm -rf /var/cache/apk/* \
|
&& rm -rf /var/cache/apk/* \
|
||||||
&& pip install --no-cache-dir -U platformio \
|
&& pip install --no-cache-dir -U platformio \
|
||||||
&& mkdir /tmp/firmware
|
&& mkdir /tmp/firmware
|
||||||
@@ -42,7 +42,7 @@ USER root
|
|||||||
|
|
||||||
RUN apk --no-cache add \
|
RUN apk --no-cache add \
|
||||||
shadow libstdc++ libbsd libgpiod yaml-cpp libusb \
|
shadow libstdc++ libbsd libgpiod yaml-cpp libusb \
|
||||||
i2c-tools libuv libx11 libinput libxkbcommon \
|
i2c-tools libuv libx11 libinput libxkbcommon sdl2 \
|
||||||
&& rm -rf /var/cache/apk/* \
|
&& rm -rf /var/cache/apk/* \
|
||||||
&& mkdir -p /var/lib/meshtasticd \
|
&& mkdir -p /var/lib/meshtasticd \
|
||||||
&& mkdir -p /etc/meshtasticd/config.d \
|
&& mkdir -p /etc/meshtasticd/config.d \
|
||||||
|
|||||||
+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.
|
||||||
|
|||||||
@@ -131,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"
|
||||||
|
|||||||
@@ -87,6 +87,9 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="2.7.20" date="2026-02-11">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.20</url>
|
||||||
|
</release>
|
||||||
<release version="2.7.19" date="2026-01-22">
|
<release version="2.7.19" date="2026-01-22">
|
||||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.19</url>
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.19</url>
|
||||||
</release>
|
</release>
|
||||||
|
|||||||
@@ -32,7 +32,8 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd"
|
"svd_path": "nrf52840.svd",
|
||||||
|
"openocd_target": "nrf52840-mdk-rs"
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "Minesemi ME25LS01",
|
"name": "Minesemi ME25LS01",
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd"
|
"svd_path": "nrf52840.svd",
|
||||||
|
"openocd_target": "nrf52840-mdk-rs"
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "MeshLink",
|
"name": "MeshLink",
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd"
|
"svd_path": "nrf52840.svd",
|
||||||
|
"openocd_target": "nrf52840-mdk-rs"
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "Minimesh Lite",
|
"name": "Minimesh Lite",
|
||||||
|
|||||||
+2
-1
@@ -32,7 +32,8 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd"
|
"svd_path": "nrf52840.svd",
|
||||||
|
"openocd_target": "nrf52840-mdk-rs"
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "MINEWSEMI_MS24SF1_SX1262",
|
"name": "MINEWSEMI_MS24SF1_SX1262",
|
||||||
|
|||||||
@@ -32,7 +32,8 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd"
|
"svd_path": "nrf52840.svd",
|
||||||
|
"openocd_target": "nrf52840-mdk-rs"
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "BQ nRF52840",
|
"name": "BQ nRF52840",
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd"
|
"svd_path": "nrf52840.svd",
|
||||||
|
"openocd_target": "nrf52840-mdk-rs"
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "ProMicro compatible nRF52840",
|
"name": "ProMicro compatible nRF52840",
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd"
|
"svd_path": "nrf52840.svd",
|
||||||
|
"openocd_target": "nrf52840-mdk-rs"
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "Seeed T1000-E",
|
"name": "Seeed T1000-E",
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd"
|
"svd_path": "nrf52840.svd",
|
||||||
|
"openocd_target": "nrf52840-mdk-rs"
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino", "freertos"],
|
"frameworks": ["arduino", "freertos"],
|
||||||
"name": "Seeed WIO WM1110",
|
"name": "Seeed WIO WM1110",
|
||||||
|
|||||||
@@ -32,7 +32,8 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd"
|
"svd_path": "nrf52840.svd",
|
||||||
|
"openocd_target": "nrf52840-mdk-rs"
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "Seeed WIO WM1110",
|
"name": "Seeed WIO WM1110",
|
||||||
|
|||||||
@@ -32,7 +32,8 @@
|
|||||||
"connectivity": ["bluetooth"],
|
"connectivity": ["bluetooth"],
|
||||||
"debug": {
|
"debug": {
|
||||||
"jlink_device": "nRF52840_xxAA",
|
"jlink_device": "nRF52840_xxAA",
|
||||||
"svd_path": "nrf52840.svd"
|
"svd_path": "nrf52840.svd",
|
||||||
|
"openocd_target": "nrf52840-mdk-rs"
|
||||||
},
|
},
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "Seeed WIO WM1110",
|
"name": "Seeed WIO WM1110",
|
||||||
|
|||||||
Vendored
+6
@@ -1,3 +1,9 @@
|
|||||||
|
meshtasticd (2.7.20.0) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Version 2.7.20
|
||||||
|
|
||||||
|
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Wed, 11 Feb 2026 12:19:54 +0000
|
||||||
|
|
||||||
meshtasticd (2.7.19.0) unstable; urgency=medium
|
meshtasticd (2.7.19.0) unstable; urgency=medium
|
||||||
|
|
||||||
* Version 2.7.19
|
* Version 2.7.19
|
||||||
|
|||||||
Vendored
+2
-1
@@ -26,7 +26,8 @@ Build-Depends: debhelper-compat (= 13),
|
|||||||
libx11-dev,
|
libx11-dev,
|
||||||
libinput-dev,
|
libinput-dev,
|
||||||
libxkbcommon-x11-dev,
|
libxkbcommon-x11-dev,
|
||||||
libsqlite3-dev
|
libsqlite3-dev,
|
||||||
|
libsdl2-dev
|
||||||
Standards-Version: 4.6.2
|
Standards-Version: 4.6.2
|
||||||
Homepage: https://github.com/meshtastic/firmware
|
Homepage: https://github.com/meshtastic/firmware
|
||||||
Rules-Requires-Root: no
|
Rules-Requires-Root: no
|
||||||
|
|||||||
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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ BuildRequires: pkgconfig(libulfius)
|
|||||||
BuildRequires: pkgconfig(x11)
|
BuildRequires: pkgconfig(x11)
|
||||||
BuildRequires: pkgconfig(libinput)
|
BuildRequires: pkgconfig(libinput)
|
||||||
BuildRequires: pkgconfig(xkbcommon-x11)
|
BuildRequires: pkgconfig(xkbcommon-x11)
|
||||||
|
BuildRequires: pkgconfig(sdl2)
|
||||||
|
|
||||||
# libbsd is needed on older Fedora/RHEL to provide 'strlcpy'
|
# libbsd is needed on older Fedora/RHEL to provide 'strlcpy'
|
||||||
%if 0%{?fedora} >= 39 || 0%{?rhel} >= 10
|
%if 0%{?fedora} >= 39 || 0%{?rhel} >= 10
|
||||||
@@ -59,8 +60,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 +158,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
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Vite cache
|
||||||
|
.vite/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# Meshtastic OLED Emulator
|
||||||
|
|
||||||
|
A Vue.js component and TypeScript library for emulating SSD1306/SH1106 OLED displays used in Meshtastic devices. This allows firmware UI development and testing directly in the browser.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Same API as firmware**: Uses the same `OLEDDisplay` API as esp8266-oled-ssd1306 library
|
||||||
|
- **Multiple display sizes**: 128x64, 128x32, 64x48, 128x128
|
||||||
|
- **Realistic rendering**: Pixel glow, OLED color tints (blue, white, yellow/blue dual)
|
||||||
|
- **XBM image support**: Render firmware icons and images
|
||||||
|
- **Font rendering**: Bitmap font support with text alignment
|
||||||
|
- **Vue 3 component**: Easy integration with Vue applications
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @meshtastic/oled-emulator
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import {
|
||||||
|
OLEDDisplay,
|
||||||
|
OLEDEmulator,
|
||||||
|
ArialMT_Plain_10,
|
||||||
|
} from "@meshtastic/oled-emulator";
|
||||||
|
|
||||||
|
// Create display matching firmware
|
||||||
|
const display = ref(new OLEDDisplay("128x64"));
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
display.value.setFont(ArialMT_Plain_10);
|
||||||
|
display.value.clear();
|
||||||
|
display.value.drawString(10, 10, "Hello Mesh!");
|
||||||
|
display.value.drawRect(5, 5, 118, 54);
|
||||||
|
display.value.fillCircle(64, 32, 15);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<OLEDEmulator
|
||||||
|
:display="display"
|
||||||
|
:pixel-scale="4"
|
||||||
|
preset="ssd1306-blue"
|
||||||
|
:enable-glow="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## OLEDDisplay API
|
||||||
|
|
||||||
|
The `OLEDDisplay` class provides the same methods as the firmware's display library:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const display = new OLEDDisplay("128x64");
|
||||||
|
|
||||||
|
// Basic drawing
|
||||||
|
display.clear();
|
||||||
|
display.setPixel(x, y);
|
||||||
|
display.drawLine(x0, y0, x1, y1);
|
||||||
|
display.drawRect(x, y, width, height);
|
||||||
|
display.fillRect(x, y, width, height);
|
||||||
|
display.drawCircle(x, y, radius);
|
||||||
|
display.fillCircle(x, y, radius);
|
||||||
|
|
||||||
|
// Text
|
||||||
|
display.setFont(ArialMT_Plain_10);
|
||||||
|
display.setTextAlignment("TEXT_ALIGN_CENTER");
|
||||||
|
display.drawString(x, y, "Hello");
|
||||||
|
display.drawStringMaxWidth(x, y, maxWidth, "Long text...");
|
||||||
|
const width = display.getStringWidth("text");
|
||||||
|
|
||||||
|
// Images (XBM format)
|
||||||
|
display.drawXbm(x, y, width, height, imageData);
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
display.setColor("WHITE"); // or 'BLACK', 'INVERSE'
|
||||||
|
|
||||||
|
// Buffer access (for custom rendering)
|
||||||
|
const buffer = display.getBuffer(); // Uint8Array
|
||||||
|
```
|
||||||
|
|
||||||
|
## Display Presets
|
||||||
|
|
||||||
|
The emulator includes several realistic display presets:
|
||||||
|
|
||||||
|
| Preset | Description |
|
||||||
|
| --------------------- | ------------------------------- |
|
||||||
|
| `ssd1306-blue` | Classic blue OLED (default) |
|
||||||
|
| `ssd1306-white` | White OLED |
|
||||||
|
| `ssd1306-yellow-blue` | Dual-color (top 16 rows yellow) |
|
||||||
|
| `sh1106` | SH1106 style |
|
||||||
|
| `sh1107` | SH1107 128x128 |
|
||||||
|
| `ssd1306-64x48` | Small 64x48 display |
|
||||||
|
| `ssd1306-128x32` | Wide 128x32 display |
|
||||||
|
|
||||||
|
## Screen Renderers
|
||||||
|
|
||||||
|
Pre-built screen renderers matching firmware UI:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ScreenRenderers } from "@meshtastic/oled-emulator";
|
||||||
|
|
||||||
|
// Boot screen
|
||||||
|
ScreenRenderers.drawBootScreen(display, "2.5.0");
|
||||||
|
|
||||||
|
// Node info screen
|
||||||
|
ScreenRenderers.drawNodeInfoScreen(display, {
|
||||||
|
shortName: "MESH",
|
||||||
|
longName: "My Node",
|
||||||
|
nodeId: "!abcd1234",
|
||||||
|
lastHeard: "2m ago",
|
||||||
|
snr: 12.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Message screen
|
||||||
|
ScreenRenderers.drawMessageScreen(display, {
|
||||||
|
from: "Alice",
|
||||||
|
text: "Hello from the mesh!",
|
||||||
|
time: "14:32",
|
||||||
|
channel: "LongFast",
|
||||||
|
});
|
||||||
|
|
||||||
|
// And more: drawGPSScreen, drawNodeListScreen, drawSystemScreen, drawCompassScreen
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Run development server
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build library
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future: Protocol Bridge
|
||||||
|
|
||||||
|
A future enhancement will allow connecting this emulator to a real Meshtastic firmware instance (running on native/portduino), enabling live framebuffer streaming over WebSocket for real-time display mirroring.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GPL-3.0 - Same as Meshtastic firmware
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Meshtastic OLED Emulator</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: #121212;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+2211
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "@meshtastic/oled-emulator",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Vue.js OLED display emulator for Meshtastic firmware UI development",
|
||||||
|
"keywords": [
|
||||||
|
"meshtastic",
|
||||||
|
"oled",
|
||||||
|
"ssd1306",
|
||||||
|
"sh1106",
|
||||||
|
"display",
|
||||||
|
"emulator",
|
||||||
|
"vue",
|
||||||
|
"vue3"
|
||||||
|
],
|
||||||
|
"author": "Meshtastic",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/meshtastic/firmware.git",
|
||||||
|
"directory": "oled-emulator"
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
},
|
||||||
|
"./style.css": "./dist/style.css"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"type-check": "vue-tsc --noEmit"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
|
"typescript": "^5.3.0",
|
||||||
|
"vite": "^5.0.0",
|
||||||
|
"vite-plugin-dts": "^3.7.0",
|
||||||
|
"vue": "^3.4.0",
|
||||||
|
"vue-tsc": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* DisplayPresets.ts - OLED display presets for emulator
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { OLEDDISPLAY_GEOMETRY } from "./OLEDDisplay";
|
||||||
|
|
||||||
|
// Display type presets with realistic OLED colors
|
||||||
|
export interface DisplayPreset {
|
||||||
|
name: string;
|
||||||
|
geometry: OLEDDISPLAY_GEOMETRY;
|
||||||
|
pixelOn: { r: number; g: number; b: number };
|
||||||
|
pixelOff: { r: number; g: number; b: number };
|
||||||
|
glow: number;
|
||||||
|
contrast: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DISPLAY_PRESETS: Record<string, DisplayPreset> = {
|
||||||
|
// Classic blue OLED (SSD1306)
|
||||||
|
"ssd1306-blue": {
|
||||||
|
name: "SSD1306 Blue",
|
||||||
|
geometry: "128x64",
|
||||||
|
pixelOn: { r: 100, g: 180, b: 255 },
|
||||||
|
pixelOff: { r: 2, g: 3, b: 8 },
|
||||||
|
glow: 0.4,
|
||||||
|
contrast: 1.0,
|
||||||
|
},
|
||||||
|
// White OLED (SSD1306)
|
||||||
|
"ssd1306-white": {
|
||||||
|
name: "SSD1306 White",
|
||||||
|
geometry: "128x64",
|
||||||
|
pixelOn: { r: 255, g: 255, b: 245 },
|
||||||
|
pixelOff: { r: 5, g: 5, b: 5 },
|
||||||
|
glow: 0.3,
|
||||||
|
contrast: 1.0,
|
||||||
|
},
|
||||||
|
// Yellow-blue dual color OLED
|
||||||
|
"ssd1306-yellow-blue": {
|
||||||
|
name: "SSD1306 Yellow/Blue",
|
||||||
|
geometry: "128x64",
|
||||||
|
// Yellow section is top 16 rows, blue is rest
|
||||||
|
pixelOn: { r: 255, g: 220, b: 50 }, // Will be overridden per-pixel
|
||||||
|
pixelOff: { r: 3, g: 3, b: 5 },
|
||||||
|
glow: 0.35,
|
||||||
|
contrast: 1.0,
|
||||||
|
},
|
||||||
|
// SH1106 (slightly different characteristics)
|
||||||
|
sh1106: {
|
||||||
|
name: "SH1106",
|
||||||
|
geometry: "128x64",
|
||||||
|
pixelOn: { r: 120, g: 200, b: 255 },
|
||||||
|
pixelOff: { r: 2, g: 2, b: 6 },
|
||||||
|
glow: 0.45,
|
||||||
|
contrast: 0.95,
|
||||||
|
},
|
||||||
|
// SH1107 128x128
|
||||||
|
sh1107: {
|
||||||
|
name: "SH1107 128x128",
|
||||||
|
geometry: "128x128",
|
||||||
|
pixelOn: { r: 255, g: 255, b: 240 },
|
||||||
|
pixelOff: { r: 4, g: 4, b: 6 },
|
||||||
|
glow: 0.3,
|
||||||
|
contrast: 1.0,
|
||||||
|
},
|
||||||
|
// Small 64x48 OLED
|
||||||
|
"ssd1306-64x48": {
|
||||||
|
name: "SSD1306 64x48",
|
||||||
|
geometry: "64x48",
|
||||||
|
pixelOn: { r: 100, g: 180, b: 255 },
|
||||||
|
pixelOff: { r: 2, g: 3, b: 8 },
|
||||||
|
glow: 0.4,
|
||||||
|
contrast: 1.0,
|
||||||
|
},
|
||||||
|
// Mini 128x32
|
||||||
|
"ssd1306-128x32": {
|
||||||
|
name: "SSD1306 128x32",
|
||||||
|
geometry: "128x32",
|
||||||
|
pixelOn: { r: 100, g: 180, b: 255 },
|
||||||
|
pixelOff: { r: 2, g: 3, b: 8 },
|
||||||
|
glow: 0.4,
|
||||||
|
contrast: 1.0,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,557 @@
|
|||||||
|
/**
|
||||||
|
* OLEDDisplay.ts - TypeScript port of esp8266-oled-ssd1306 OLEDDisplay API
|
||||||
|
*
|
||||||
|
* This provides the same drawing API as the firmware uses, allowing
|
||||||
|
* Meshtastic screen rendering code to be ported to the web.
|
||||||
|
*
|
||||||
|
* Buffer format: Vertical byte packing (8 vertical pixels per byte)
|
||||||
|
* Same as SSD1306/SH1106 native format.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type OLEDDISPLAY_GEOMETRY =
|
||||||
|
| "128x64"
|
||||||
|
| "128x32"
|
||||||
|
| "64x48"
|
||||||
|
| "64x32"
|
||||||
|
| "128x128"
|
||||||
|
| "96x16";
|
||||||
|
|
||||||
|
export type OLEDDISPLAY_COLOR = "WHITE" | "BLACK" | "INVERSE";
|
||||||
|
|
||||||
|
export type OLEDDISPLAY_TEXT_ALIGNMENT =
|
||||||
|
| "TEXT_ALIGN_LEFT"
|
||||||
|
| "TEXT_ALIGN_CENTER"
|
||||||
|
| "TEXT_ALIGN_RIGHT";
|
||||||
|
|
||||||
|
export interface OLEDFont {
|
||||||
|
height: number;
|
||||||
|
firstChar: number;
|
||||||
|
charCount: number;
|
||||||
|
widths: number[];
|
||||||
|
data: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolution info based on geometry
|
||||||
|
const GEOMETRY_SIZES: Record<
|
||||||
|
OLEDDISPLAY_GEOMETRY,
|
||||||
|
{ width: number; height: number }
|
||||||
|
> = {
|
||||||
|
"128x64": { width: 128, height: 64 },
|
||||||
|
"128x32": { width: 128, height: 32 },
|
||||||
|
"64x48": { width: 64, height: 48 },
|
||||||
|
"64x32": { width: 64, height: 32 },
|
||||||
|
"128x128": { width: 128, height: 128 },
|
||||||
|
"96x16": { width: 96, height: 16 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export class OLEDDisplay {
|
||||||
|
private buffer: Uint8Array;
|
||||||
|
private _width: number;
|
||||||
|
private _height: number;
|
||||||
|
private color: OLEDDISPLAY_COLOR = "WHITE";
|
||||||
|
private textAlignment: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_LEFT";
|
||||||
|
private font: OLEDFont | null = null;
|
||||||
|
|
||||||
|
constructor(geometry: OLEDDISPLAY_GEOMETRY = "128x64") {
|
||||||
|
const size = GEOMETRY_SIZES[geometry];
|
||||||
|
this._width = size.width;
|
||||||
|
this._height = size.height;
|
||||||
|
// Buffer size: width * (height / 8) - vertical byte packing
|
||||||
|
this.buffer = new Uint8Array((this._width * this._height) / 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
get width(): number {
|
||||||
|
return this._width;
|
||||||
|
}
|
||||||
|
|
||||||
|
get height(): number {
|
||||||
|
return this._height;
|
||||||
|
}
|
||||||
|
|
||||||
|
getWidth(): number {
|
||||||
|
return this._width;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHeight(): number {
|
||||||
|
return this._height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the raw framebuffer (vertical byte packing format)
|
||||||
|
*/
|
||||||
|
getBuffer(): Uint8Array {
|
||||||
|
return this.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBufferSize(): number {
|
||||||
|
return this.buffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the display buffer
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
this.buffer.fill(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the drawing color
|
||||||
|
*/
|
||||||
|
setColor(color: OLEDDISPLAY_COLOR): void {
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set text alignment for drawString
|
||||||
|
*/
|
||||||
|
setTextAlignment(align: OLEDDISPLAY_TEXT_ALIGNMENT): void {
|
||||||
|
this.textAlignment = align;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current font
|
||||||
|
*/
|
||||||
|
setFont(font: OLEDFont): void {
|
||||||
|
this.font = font;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a single pixel
|
||||||
|
*/
|
||||||
|
setPixel(x: number, y: number): void {
|
||||||
|
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return;
|
||||||
|
|
||||||
|
// Vertical byte packing: each byte represents 8 vertical pixels
|
||||||
|
const byteIndex = x + Math.floor(y / 8) * this._width;
|
||||||
|
const bitMask = 1 << (y & 7);
|
||||||
|
|
||||||
|
switch (this.color) {
|
||||||
|
case "WHITE":
|
||||||
|
this.buffer[byteIndex] |= bitMask;
|
||||||
|
break;
|
||||||
|
case "BLACK":
|
||||||
|
this.buffer[byteIndex] &= ~bitMask;
|
||||||
|
break;
|
||||||
|
case "INVERSE":
|
||||||
|
this.buffer[byteIndex] ^= bitMask;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear a single pixel (set to black)
|
||||||
|
*/
|
||||||
|
clearPixel(x: number, y: number): void {
|
||||||
|
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return;
|
||||||
|
const byteIndex = x + Math.floor(y / 8) * this._width;
|
||||||
|
const bitMask = 1 << (y & 7);
|
||||||
|
this.buffer[byteIndex] &= ~bitMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a pixel value (true = on, false = off)
|
||||||
|
*/
|
||||||
|
getPixel(x: number, y: number): boolean {
|
||||||
|
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return false;
|
||||||
|
const byteIndex = x + Math.floor(y / 8) * this._width;
|
||||||
|
const bitMask = 1 << (y & 7);
|
||||||
|
return (this.buffer[byteIndex] & bitMask) !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a line using Bresenham's algorithm
|
||||||
|
*/
|
||||||
|
drawLine(x0: number, y0: number, x1: number, y1: number): void {
|
||||||
|
const dx = Math.abs(x1 - x0);
|
||||||
|
const dy = Math.abs(y1 - y0);
|
||||||
|
const sx = x0 < x1 ? 1 : -1;
|
||||||
|
const sy = y0 < y1 ? 1 : -1;
|
||||||
|
let err = dx - dy;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
this.setPixel(x0, y0);
|
||||||
|
if (x0 === x1 && y0 === y1) break;
|
||||||
|
const e2 = 2 * err;
|
||||||
|
if (e2 > -dy) {
|
||||||
|
err -= dy;
|
||||||
|
x0 += sx;
|
||||||
|
}
|
||||||
|
if (e2 < dx) {
|
||||||
|
err += dx;
|
||||||
|
y0 += sy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw horizontal line (optimized)
|
||||||
|
*/
|
||||||
|
drawHorizontalLine(x: number, y: number, length: number): void {
|
||||||
|
if (y < 0 || y >= this._height) return;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
this.setPixel(x + i, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw vertical line (optimized)
|
||||||
|
*/
|
||||||
|
drawVerticalLine(x: number, y: number, length: number): void {
|
||||||
|
if (x < 0 || x >= this._width) return;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
this.setPixel(x, y + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a rectangle outline
|
||||||
|
*/
|
||||||
|
drawRect(x: number, y: number, width: number, height: number): void {
|
||||||
|
this.drawHorizontalLine(x, y, width);
|
||||||
|
this.drawHorizontalLine(x, y + height - 1, width);
|
||||||
|
this.drawVerticalLine(x, y, height);
|
||||||
|
this.drawVerticalLine(x + width - 1, y, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a filled rectangle
|
||||||
|
*/
|
||||||
|
fillRect(x: number, y: number, width: number, height: number): void {
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
for (let j = 0; j < height; j++) {
|
||||||
|
this.setPixel(x + i, y + j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a circle outline using midpoint algorithm
|
||||||
|
*/
|
||||||
|
drawCircle(x0: number, y0: number, radius: number): void {
|
||||||
|
let x = radius;
|
||||||
|
let y = 0;
|
||||||
|
let err = 0;
|
||||||
|
|
||||||
|
while (x >= y) {
|
||||||
|
this.setPixel(x0 + x, y0 + y);
|
||||||
|
this.setPixel(x0 + y, y0 + x);
|
||||||
|
this.setPixel(x0 - y, y0 + x);
|
||||||
|
this.setPixel(x0 - x, y0 + y);
|
||||||
|
this.setPixel(x0 - x, y0 - y);
|
||||||
|
this.setPixel(x0 - y, y0 - x);
|
||||||
|
this.setPixel(x0 + y, y0 - x);
|
||||||
|
this.setPixel(x0 + x, y0 - y);
|
||||||
|
|
||||||
|
y++;
|
||||||
|
if (err <= 0) {
|
||||||
|
err += 2 * y + 1;
|
||||||
|
}
|
||||||
|
if (err > 0) {
|
||||||
|
x--;
|
||||||
|
err -= 2 * x + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a filled circle
|
||||||
|
*/
|
||||||
|
fillCircle(x0: number, y0: number, radius: number): void {
|
||||||
|
let x = radius;
|
||||||
|
let y = 0;
|
||||||
|
let err = 0;
|
||||||
|
|
||||||
|
while (x >= y) {
|
||||||
|
this.drawHorizontalLine(x0 - x, y0 + y, 2 * x + 1);
|
||||||
|
this.drawHorizontalLine(x0 - y, y0 + x, 2 * y + 1);
|
||||||
|
this.drawHorizontalLine(x0 - x, y0 - y, 2 * x + 1);
|
||||||
|
this.drawHorizontalLine(x0 - y, y0 - x, 2 * y + 1);
|
||||||
|
|
||||||
|
y++;
|
||||||
|
if (err <= 0) {
|
||||||
|
err += 2 * y + 1;
|
||||||
|
}
|
||||||
|
if (err > 0) {
|
||||||
|
x--;
|
||||||
|
err -= 2 * x + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a quarter circle (quadrant 0-3)
|
||||||
|
*/
|
||||||
|
drawCircleQuads(x0: number, y0: number, radius: number, quads: number): void {
|
||||||
|
let x = radius;
|
||||||
|
let y = 0;
|
||||||
|
let err = 0;
|
||||||
|
|
||||||
|
while (x >= y) {
|
||||||
|
if (quads & 0x1) {
|
||||||
|
this.setPixel(x0 + x, y0 - y);
|
||||||
|
this.setPixel(x0 + y, y0 - x);
|
||||||
|
}
|
||||||
|
if (quads & 0x2) {
|
||||||
|
this.setPixel(x0 - y, y0 - x);
|
||||||
|
this.setPixel(x0 - x, y0 - y);
|
||||||
|
}
|
||||||
|
if (quads & 0x4) {
|
||||||
|
this.setPixel(x0 - x, y0 + y);
|
||||||
|
this.setPixel(x0 - y, y0 + x);
|
||||||
|
}
|
||||||
|
if (quads & 0x8) {
|
||||||
|
this.setPixel(x0 + y, y0 + x);
|
||||||
|
this.setPixel(x0 + x, y0 + y);
|
||||||
|
}
|
||||||
|
|
||||||
|
y++;
|
||||||
|
if (err <= 0) {
|
||||||
|
err += 2 * y + 1;
|
||||||
|
}
|
||||||
|
if (err > 0) {
|
||||||
|
x--;
|
||||||
|
err -= 2 * x + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw an XBM image (like firmware's drawXbm)
|
||||||
|
* XBM format: LSB first, horizontal
|
||||||
|
*/
|
||||||
|
drawXbm(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
xbm: Uint8Array | number[],
|
||||||
|
): void {
|
||||||
|
const widthBytes = Math.ceil(width / 8);
|
||||||
|
for (let j = 0; j < height; j++) {
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
const byteIndex = j * widthBytes + Math.floor(i / 8);
|
||||||
|
const bitIndex = i % 8;
|
||||||
|
if (xbm[byteIndex] & (1 << bitIndex)) {
|
||||||
|
this.setPixel(x + i, y + j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a PROGMEM-style image (vertical byte format, like some firmware icons)
|
||||||
|
*/
|
||||||
|
drawFastImage(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
image: Uint8Array | number[],
|
||||||
|
): void {
|
||||||
|
const heightInBytes = Math.ceil(height / 8);
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
for (let j = 0; j < heightInBytes; j++) {
|
||||||
|
const imageByte = image[i + j * width];
|
||||||
|
for (let bit = 0; bit < 8; bit++) {
|
||||||
|
if (imageByte & (1 << bit)) {
|
||||||
|
this.setPixel(x + i, y + j * 8 + bit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the width of a string with the current font
|
||||||
|
*/
|
||||||
|
getStringWidth(text: string): number {
|
||||||
|
if (!this.font) return 0;
|
||||||
|
let width = 0;
|
||||||
|
for (const char of text) {
|
||||||
|
const charCode = char.charCodeAt(0);
|
||||||
|
if (
|
||||||
|
charCode >= this.font.firstChar &&
|
||||||
|
charCode < this.font.firstChar + this.font.charCount
|
||||||
|
) {
|
||||||
|
width += this.font.widths[charCode - this.font.firstChar];
|
||||||
|
} else {
|
||||||
|
// Unknown char - use space width or default
|
||||||
|
width += this.font.widths[0] || 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a string at the given position
|
||||||
|
*/
|
||||||
|
drawString(x: number, y: number, text: string): void {
|
||||||
|
if (!this.font) return;
|
||||||
|
|
||||||
|
// Adjust x based on alignment
|
||||||
|
let startX = x;
|
||||||
|
if (this.textAlignment === "TEXT_ALIGN_CENTER") {
|
||||||
|
startX = x - Math.floor(this.getStringWidth(text) / 2);
|
||||||
|
} else if (this.textAlignment === "TEXT_ALIGN_RIGHT") {
|
||||||
|
startX = x - this.getStringWidth(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cursorX = startX;
|
||||||
|
for (const char of text) {
|
||||||
|
const charCode = char.charCodeAt(0);
|
||||||
|
if (
|
||||||
|
charCode >= this.font.firstChar &&
|
||||||
|
charCode < this.font.firstChar + this.font.charCount
|
||||||
|
) {
|
||||||
|
const charIndex = charCode - this.font.firstChar;
|
||||||
|
const charWidth = this.font.widths[charIndex];
|
||||||
|
|
||||||
|
// Find the offset into the font data
|
||||||
|
let offset = 0;
|
||||||
|
const bytesPerColumn = Math.ceil(this.font.height / 8);
|
||||||
|
for (let i = 0; i < charIndex; i++) {
|
||||||
|
offset += this.font.widths[i] * bytesPerColumn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the character
|
||||||
|
this.drawFontChar(
|
||||||
|
cursorX,
|
||||||
|
y,
|
||||||
|
charWidth,
|
||||||
|
this.font.height,
|
||||||
|
this.font.data,
|
||||||
|
offset,
|
||||||
|
);
|
||||||
|
cursorX += charWidth;
|
||||||
|
} else {
|
||||||
|
// Unknown character - advance by space
|
||||||
|
cursorX += this.font.widths[0] || 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a single font character
|
||||||
|
*/
|
||||||
|
private drawFontChar(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
data: Uint8Array,
|
||||||
|
offset: number,
|
||||||
|
): void {
|
||||||
|
const bytesPerColumn = Math.ceil(height / 8);
|
||||||
|
for (let col = 0; col < width; col++) {
|
||||||
|
for (let byteRow = 0; byteRow < bytesPerColumn; byteRow++) {
|
||||||
|
const dataByte = data[offset + col * bytesPerColumn + byteRow];
|
||||||
|
for (let bit = 0; bit < 8; bit++) {
|
||||||
|
const pixelY = byteRow * 8 + bit;
|
||||||
|
if (pixelY < height && dataByte & (1 << bit)) {
|
||||||
|
this.setPixel(x + col, y + pixelY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a string that wraps within maxWidth
|
||||||
|
*/
|
||||||
|
drawStringMaxWidth(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
maxWidth: number,
|
||||||
|
text: string,
|
||||||
|
): void {
|
||||||
|
if (!this.font) return;
|
||||||
|
|
||||||
|
const words = text.split(" ");
|
||||||
|
let line = "";
|
||||||
|
let lineY = y;
|
||||||
|
|
||||||
|
for (const word of words) {
|
||||||
|
const testLine = line + (line ? " " : "") + word;
|
||||||
|
const testWidth = this.getStringWidth(testLine);
|
||||||
|
|
||||||
|
if (testWidth > maxWidth && line) {
|
||||||
|
this.drawString(x, lineY, line);
|
||||||
|
line = word;
|
||||||
|
lineY += this.font.height;
|
||||||
|
} else {
|
||||||
|
line = testLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line) {
|
||||||
|
this.drawString(x, lineY, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a progress bar
|
||||||
|
*/
|
||||||
|
drawProgressBar(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
progress: number,
|
||||||
|
): void {
|
||||||
|
// Clamp progress 0-100
|
||||||
|
progress = Math.max(0, Math.min(100, progress));
|
||||||
|
|
||||||
|
// Draw outline
|
||||||
|
this.drawRect(x, y, width, height);
|
||||||
|
|
||||||
|
// Draw fill
|
||||||
|
const fillWidth = Math.floor(((width - 4) * progress) / 100);
|
||||||
|
this.fillRect(x + 2, y + 2, fillWidth, height - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display - no-op for emulator (would send buffer to hardware)
|
||||||
|
*/
|
||||||
|
display(): void {
|
||||||
|
// In real hardware this sends the buffer to the display
|
||||||
|
// For emulator, this is a no-op - we render from getBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flip the display (useful for some mounting orientations)
|
||||||
|
*/
|
||||||
|
flipScreenVertically(): void {
|
||||||
|
// Rotate buffer 180 degrees
|
||||||
|
const newBuffer = new Uint8Array(this.buffer.length);
|
||||||
|
const heightBytes = this._height / 8;
|
||||||
|
|
||||||
|
for (let x = 0; x < this._width; x++) {
|
||||||
|
for (let yByte = 0; yByte < heightBytes; yByte++) {
|
||||||
|
let srcByte = this.buffer[x + yByte * this._width];
|
||||||
|
// Reverse the bits
|
||||||
|
let reversed = 0;
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
if (srcByte & (1 << i)) {
|
||||||
|
reversed |= 1 << (7 - i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Place in mirrored position
|
||||||
|
const newX = this._width - 1 - x;
|
||||||
|
const newYByte = heightBytes - 1 - yByte;
|
||||||
|
newBuffer[newX + newYByte * this._width] = reversed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buffer = newBuffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color constants for compatibility with firmware code style
|
||||||
|
export const WHITE: OLEDDISPLAY_COLOR = "WHITE";
|
||||||
|
export const BLACK: OLEDDISPLAY_COLOR = "BLACK";
|
||||||
|
export const INVERSE: OLEDDISPLAY_COLOR = "INVERSE";
|
||||||
|
|
||||||
|
export const TEXT_ALIGN_LEFT: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_LEFT";
|
||||||
|
export const TEXT_ALIGN_CENTER: OLEDDISPLAY_TEXT_ALIGNMENT =
|
||||||
|
"TEXT_ALIGN_CENTER";
|
||||||
|
export const TEXT_ALIGN_RIGHT: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_RIGHT";
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* OLEDEmulator.vue - Vue 3 component for OLED display emulation
|
||||||
|
*
|
||||||
|
* Renders an OLEDDisplay framebuffer with realistic OLED visual effects
|
||||||
|
* including pixel glow, color tinting, and various display sizes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, computed, watch, onMounted, onUnmounted, type PropType } from 'vue';
|
||||||
|
import { OLEDDisplay } from './OLEDDisplay';
|
||||||
|
import { DISPLAY_PRESETS, type DisplayPreset } from './DisplayPresets';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/**
|
||||||
|
* The OLEDDisplay instance to render
|
||||||
|
*/
|
||||||
|
display: {
|
||||||
|
type: Object as PropType<OLEDDisplay>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Pixel scale factor (how many canvas pixels per OLED pixel)
|
||||||
|
*/
|
||||||
|
pixelScale: {
|
||||||
|
type: Number,
|
||||||
|
default: 4,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Display preset name or custom preset object
|
||||||
|
*/
|
||||||
|
preset: {
|
||||||
|
type: [String, Object] as PropType<string | DisplayPreset>,
|
||||||
|
default: 'ssd1306-blue',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Enable pixel glow effect (slight bloom around lit pixels)
|
||||||
|
*/
|
||||||
|
enableGlow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Enable sub-pixel rendering for more realistic look
|
||||||
|
*/
|
||||||
|
enableSubPixel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Show pixel grid lines
|
||||||
|
*/
|
||||||
|
showGrid: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Auto-refresh interval in ms (0 = manual refresh only)
|
||||||
|
*/
|
||||||
|
refreshInterval: {
|
||||||
|
type: Number,
|
||||||
|
default: 50,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Yellow/blue split mode for dual-color OLEDs (rows 0-15 yellow)
|
||||||
|
*/
|
||||||
|
dualColor: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'click', x: number, y: number): void;
|
||||||
|
(e: 'refresh'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const canvas = ref<HTMLCanvasElement | null>(null);
|
||||||
|
const refreshTimer = ref<number | null>(null);
|
||||||
|
|
||||||
|
// Get the current display preset
|
||||||
|
const currentPreset = computed<DisplayPreset>(() => {
|
||||||
|
if (typeof props.preset === 'string') {
|
||||||
|
return DISPLAY_PRESETS[props.preset] || DISPLAY_PRESETS['ssd1306-blue'];
|
||||||
|
}
|
||||||
|
return props.preset;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Canvas dimensions
|
||||||
|
const canvasWidth = computed(() => props.display.width * props.pixelScale);
|
||||||
|
const canvasHeight = computed(() => props.display.height * props.pixelScale);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the framebuffer to canvas with OLED effects
|
||||||
|
*/
|
||||||
|
function render(): void {
|
||||||
|
const cvs = canvas.value;
|
||||||
|
if (!cvs) return;
|
||||||
|
|
||||||
|
const ctx = cvs.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const buffer = props.display.getBuffer();
|
||||||
|
const width = props.display.width;
|
||||||
|
const height = props.display.height;
|
||||||
|
const scale = props.pixelScale;
|
||||||
|
const preset = currentPreset.value;
|
||||||
|
|
||||||
|
// Create image data
|
||||||
|
const imageData = ctx.createImageData(canvasWidth.value, canvasHeight.value);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
// Yellow color for dual-color mode (top 16 rows)
|
||||||
|
const yellowOn = { r: 255, g: 220, b: 50 };
|
||||||
|
const blueOn = { r: 100, g: 180, b: 255 };
|
||||||
|
|
||||||
|
// Render each OLED pixel
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
// Get pixel from framebuffer (vertical byte packing)
|
||||||
|
const byteIndex = x + Math.floor(y / 8) * width;
|
||||||
|
const bitMask = 1 << (y & 7);
|
||||||
|
const isOn = (buffer[byteIndex] & bitMask) !== 0;
|
||||||
|
|
||||||
|
// Determine pixel color
|
||||||
|
let pixelColor: { r: number; g: number; b: number };
|
||||||
|
if (isOn) {
|
||||||
|
if (props.dualColor && y < 16) {
|
||||||
|
pixelColor = yellowOn;
|
||||||
|
} else if (props.dualColor) {
|
||||||
|
pixelColor = blueOn;
|
||||||
|
} else {
|
||||||
|
pixelColor = preset.pixelOn;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pixelColor = preset.pixelOff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply contrast
|
||||||
|
pixelColor = {
|
||||||
|
r: Math.min(255, pixelColor.r * preset.contrast),
|
||||||
|
g: Math.min(255, pixelColor.g * preset.contrast),
|
||||||
|
b: Math.min(255, pixelColor.b * preset.contrast),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Scale up pixel to canvas
|
||||||
|
for (let sy = 0; sy < scale; sy++) {
|
||||||
|
for (let sx = 0; sx < scale; sx++) {
|
||||||
|
const px = x * scale + sx;
|
||||||
|
const py = y * scale + sy;
|
||||||
|
const idx = (py * canvasWidth.value + px) * 4;
|
||||||
|
|
||||||
|
// Apply glow effect (brighter center, dimmer edges)
|
||||||
|
let intensity = 1.0;
|
||||||
|
if (props.enableGlow && isOn) {
|
||||||
|
const centerX = scale / 2;
|
||||||
|
const centerY = scale / 2;
|
||||||
|
const dist = Math.sqrt(
|
||||||
|
Math.pow(sx - centerX, 2) + Math.pow(sy - centerY, 2)
|
||||||
|
);
|
||||||
|
const maxDist = Math.sqrt(2) * (scale / 2);
|
||||||
|
intensity = 1.0 - (dist / maxDist) * preset.glow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sub-pixel rendering (RGB stripes)
|
||||||
|
if (props.enableSubPixel && isOn) {
|
||||||
|
const subPixelPos = sx % 3;
|
||||||
|
if (subPixelPos === 0) {
|
||||||
|
data[idx] = pixelColor.r * intensity * 1.2;
|
||||||
|
data[idx + 1] = pixelColor.g * intensity * 0.8;
|
||||||
|
data[idx + 2] = pixelColor.b * intensity * 0.8;
|
||||||
|
} else if (subPixelPos === 1) {
|
||||||
|
data[idx] = pixelColor.r * intensity * 0.8;
|
||||||
|
data[idx + 1] = pixelColor.g * intensity * 1.2;
|
||||||
|
data[idx + 2] = pixelColor.b * intensity * 0.8;
|
||||||
|
} else {
|
||||||
|
data[idx] = pixelColor.r * intensity * 0.8;
|
||||||
|
data[idx + 1] = pixelColor.g * intensity * 0.8;
|
||||||
|
data[idx + 2] = pixelColor.b * intensity * 1.2;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data[idx] = pixelColor.r * intensity;
|
||||||
|
data[idx + 1] = pixelColor.g * intensity;
|
||||||
|
data[idx + 2] = pixelColor.b * intensity;
|
||||||
|
}
|
||||||
|
data[idx + 3] = 255; // Alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw grid lines if enabled
|
||||||
|
if (props.showGrid) {
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
// Draw right and bottom edge of each pixel
|
||||||
|
// Right edge
|
||||||
|
const rx = (x + 1) * scale - 1;
|
||||||
|
for (let sy = 0; sy < scale; sy++) {
|
||||||
|
const py = y * scale + sy;
|
||||||
|
const idx = (py * canvasWidth.value + rx) * 4;
|
||||||
|
data[idx] = Math.min(255, data[idx] + 20);
|
||||||
|
data[idx + 1] = Math.min(255, data[idx + 1] + 20);
|
||||||
|
data[idx + 2] = Math.min(255, data[idx + 2] + 20);
|
||||||
|
}
|
||||||
|
// Bottom edge
|
||||||
|
const by = (y + 1) * scale - 1;
|
||||||
|
for (let sx = 0; sx < scale; sx++) {
|
||||||
|
const px = x * scale + sx;
|
||||||
|
const idx = (by * canvasWidth.value + px) * 4;
|
||||||
|
data[idx] = Math.min(255, data[idx] + 20);
|
||||||
|
data[idx + 1] = Math.min(255, data[idx + 1] + 20);
|
||||||
|
data[idx + 2] = Math.min(255, data[idx + 2] + 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
emit('refresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle canvas click
|
||||||
|
*/
|
||||||
|
function handleClick(event: MouseEvent): void {
|
||||||
|
const cvs = canvas.value;
|
||||||
|
if (!cvs) return;
|
||||||
|
|
||||||
|
const rect = cvs.getBoundingClientRect();
|
||||||
|
const scaleX = cvs.width / rect.width;
|
||||||
|
const scaleY = cvs.height / rect.height;
|
||||||
|
|
||||||
|
const canvasX = (event.clientX - rect.left) * scaleX;
|
||||||
|
const canvasY = (event.clientY - rect.top) * scaleY;
|
||||||
|
|
||||||
|
const oledX = Math.floor(canvasX / props.pixelScale);
|
||||||
|
const oledY = Math.floor(canvasY / props.pixelScale);
|
||||||
|
|
||||||
|
emit('click', oledX, oledY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start auto-refresh timer
|
||||||
|
*/
|
||||||
|
function startRefreshTimer(): void {
|
||||||
|
stopRefreshTimer();
|
||||||
|
if (props.refreshInterval > 0) {
|
||||||
|
refreshTimer.value = window.setInterval(render, props.refreshInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop auto-refresh timer
|
||||||
|
*/
|
||||||
|
function stopRefreshTimer(): void {
|
||||||
|
if (refreshTimer.value !== null) {
|
||||||
|
clearInterval(refreshTimer.value);
|
||||||
|
refreshTimer.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(() => {
|
||||||
|
render();
|
||||||
|
startRefreshTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopRefreshTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes
|
||||||
|
watch(() => props.refreshInterval, startRefreshTimer);
|
||||||
|
watch(() => props.display, render, { deep: true });
|
||||||
|
watch(() => props.pixelScale, render);
|
||||||
|
watch(() => props.preset, render);
|
||||||
|
watch(() => props.enableGlow, render);
|
||||||
|
watch(() => props.showGrid, render);
|
||||||
|
watch(() => props.dualColor, render);
|
||||||
|
|
||||||
|
// Expose render method for manual refresh
|
||||||
|
defineExpose({ render });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="oled-emulator-wrapper">
|
||||||
|
<canvas
|
||||||
|
ref="canvas"
|
||||||
|
:width="canvasWidth"
|
||||||
|
:height="canvasHeight"
|
||||||
|
class="oled-canvas"
|
||||||
|
@click="handleClick"
|
||||||
|
/>
|
||||||
|
<div class="oled-bezel" v-if="$slots.bezel">
|
||||||
|
<slot name="bezel" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.oled-emulator-wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow:
|
||||||
|
0 2px 8px rgba(0, 0, 0, 0.4),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oled-canvas {
|
||||||
|
display: block;
|
||||||
|
border-radius: 2px;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oled-bezel {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
/**
|
||||||
|
* OLEDFonts.ts - Font data for OLED display emulator
|
||||||
|
*
|
||||||
|
* These are simplified bitmap fonts matching the style used in
|
||||||
|
* Meshtastic firmware (based on ArialMT from esp8266-oled-ssd1306).
|
||||||
|
*
|
||||||
|
* Font format: Vertical byte packing, column-major order
|
||||||
|
* Each character is drawn column by column, with each byte representing
|
||||||
|
* 8 vertical pixels.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { OLEDFont } from "./OLEDDisplay";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ArialMT Plain 10 - Small font (height ~13 pixels)
|
||||||
|
* Covers ASCII 32-126 (space through ~)
|
||||||
|
*/
|
||||||
|
export const ArialMT_Plain_10: OLEDFont = {
|
||||||
|
height: 13,
|
||||||
|
firstChar: 32,
|
||||||
|
charCount: 95,
|
||||||
|
widths: [
|
||||||
|
3, // space
|
||||||
|
3, // !
|
||||||
|
4, // "
|
||||||
|
6, // #
|
||||||
|
6, // $
|
||||||
|
9, // %
|
||||||
|
7, // &
|
||||||
|
2, // '
|
||||||
|
4, // (
|
||||||
|
4, // )
|
||||||
|
5, // *
|
||||||
|
6, // +
|
||||||
|
3, // ,
|
||||||
|
4, // -
|
||||||
|
3, // .
|
||||||
|
4, // /
|
||||||
|
6, // 0
|
||||||
|
6, // 1
|
||||||
|
6, // 2
|
||||||
|
6, // 3
|
||||||
|
6, // 4
|
||||||
|
6, // 5
|
||||||
|
6, // 6
|
||||||
|
6, // 7
|
||||||
|
6, // 8
|
||||||
|
6, // 9
|
||||||
|
3, // :
|
||||||
|
3, // ;
|
||||||
|
6, // <
|
||||||
|
6, // =
|
||||||
|
6, // >
|
||||||
|
6, // ?
|
||||||
|
11, // @
|
||||||
|
8, // A
|
||||||
|
7, // B
|
||||||
|
7, // C
|
||||||
|
7, // D
|
||||||
|
6, // E
|
||||||
|
6, // F
|
||||||
|
8, // G
|
||||||
|
7, // H
|
||||||
|
3, // I
|
||||||
|
5, // J
|
||||||
|
7, // K
|
||||||
|
6, // L
|
||||||
|
9, // M
|
||||||
|
7, // N
|
||||||
|
8, // O
|
||||||
|
6, // P
|
||||||
|
8, // Q
|
||||||
|
7, // R
|
||||||
|
6, // S
|
||||||
|
6, // T
|
||||||
|
7, // U
|
||||||
|
7, // V
|
||||||
|
11, // W
|
||||||
|
7, // X
|
||||||
|
7, // Y
|
||||||
|
6, // Z
|
||||||
|
4, // [
|
||||||
|
4, // \
|
||||||
|
4, // ]
|
||||||
|
6, // ^
|
||||||
|
6, // _
|
||||||
|
4, // `
|
||||||
|
6, // a
|
||||||
|
6, // b
|
||||||
|
5, // c
|
||||||
|
6, // d
|
||||||
|
6, // e
|
||||||
|
4, // f
|
||||||
|
6, // g
|
||||||
|
6, // h
|
||||||
|
3, // i
|
||||||
|
3, // j
|
||||||
|
6, // k
|
||||||
|
3, // l
|
||||||
|
9, // m
|
||||||
|
6, // n
|
||||||
|
6, // o
|
||||||
|
6, // p
|
||||||
|
6, // q
|
||||||
|
4, // r
|
||||||
|
5, // s
|
||||||
|
4, // t
|
||||||
|
6, // u
|
||||||
|
6, // v
|
||||||
|
9, // w
|
||||||
|
6, // x
|
||||||
|
6, // y
|
||||||
|
5, // z
|
||||||
|
4, // {
|
||||||
|
3, // |
|
||||||
|
4, // }
|
||||||
|
6, // ~
|
||||||
|
],
|
||||||
|
data: new Uint8Array([
|
||||||
|
// Space (32)
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// ! (33)
|
||||||
|
0x00, 0x00, 0xbe, 0x00, 0x00, 0x00,
|
||||||
|
// " (34)
|
||||||
|
0x00, 0x0e, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// # (35)
|
||||||
|
0x00, 0x48, 0xf8, 0x4e, 0xf8, 0x4e, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// $ (36)
|
||||||
|
0x00, 0x9c, 0x92, 0xff, 0x92, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// % (37)
|
||||||
|
0x0c, 0x12, 0x12, 0x8c, 0x60, 0x18, 0xc6, 0x20, 0x20, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// & (38)
|
||||||
|
0x00, 0x60, 0x9c, 0x92, 0x6a, 0xa4, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
// ' (39)
|
||||||
|
0x00, 0x0e, 0x00, 0x00,
|
||||||
|
// ( (40)
|
||||||
|
0x00, 0x7c, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// ) (41)
|
||||||
|
0x00, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// * (42)
|
||||||
|
0x14, 0x08, 0x3e, 0x08, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// + (43)
|
||||||
|
0x10, 0x10, 0x7c, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// , (44)
|
||||||
|
0x00, 0x00, 0xc0, 0x00, 0x00, 0x00,
|
||||||
|
// - (45)
|
||||||
|
0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// . (46)
|
||||||
|
0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
|
||||||
|
// / (47)
|
||||||
|
0x00, 0xc0, 0x38, 0x06, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// 0 (48)
|
||||||
|
0x7c, 0x82, 0x82, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// 1 (49)
|
||||||
|
0x00, 0x04, 0x02, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// 2 (50)
|
||||||
|
0x84, 0xc2, 0xa2, 0x92, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// 3 (51)
|
||||||
|
0x44, 0x82, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// 4 (52)
|
||||||
|
0x30, 0x28, 0x24, 0xfe, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// 5 (53)
|
||||||
|
0x4e, 0x8a, 0x8a, 0x8a, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// 6 (54)
|
||||||
|
0x78, 0x94, 0x92, 0x92, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// 7 (55)
|
||||||
|
0x02, 0xc2, 0x32, 0x0a, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// 8 (56)
|
||||||
|
0x6c, 0x92, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// 9 (57)
|
||||||
|
0x0c, 0x92, 0x92, 0x52, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// : (58)
|
||||||
|
0x00, 0x00, 0x88, 0x00, 0x00, 0x00,
|
||||||
|
// ; (59)
|
||||||
|
0x00, 0x00, 0xc8, 0x00, 0x00, 0x00,
|
||||||
|
// < (60)
|
||||||
|
0x10, 0x28, 0x28, 0x44, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// = (61)
|
||||||
|
0x28, 0x28, 0x28, 0x28, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// > (62)
|
||||||
|
0x44, 0x44, 0x28, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// ? (63)
|
||||||
|
0x04, 0x02, 0xa2, 0x12, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// @ (64)
|
||||||
|
0xf0, 0x08, 0xe4, 0x12, 0x12, 0x12, 0xe2, 0x14, 0x08, 0xf0, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// A (65)
|
||||||
|
0x00, 0xc0, 0x38, 0x26, 0x26, 0x38, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
// B (66)
|
||||||
|
0xfe, 0x92, 0x92, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
// C (67)
|
||||||
|
0x7c, 0x82, 0x82, 0x82, 0x82, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
// D (68)
|
||||||
|
0xfe, 0x82, 0x82, 0x82, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
// E (69)
|
||||||
|
0xfe, 0x92, 0x92, 0x92, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// F (70)
|
||||||
|
0xfe, 0x12, 0x12, 0x12, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// G (71)
|
||||||
|
0x7c, 0x82, 0x82, 0x92, 0x92, 0x74, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
// H (72)
|
||||||
|
0xfe, 0x10, 0x10, 0x10, 0x10, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
// I (73)
|
||||||
|
0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// J (74)
|
||||||
|
0x40, 0x80, 0x80, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// K (75)
|
||||||
|
0xfe, 0x10, 0x28, 0x44, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
// L (76)
|
||||||
|
0xfe, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// M (77)
|
||||||
|
0xfe, 0x04, 0x18, 0x60, 0x60, 0x18, 0x04, 0xfe, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// N (78)
|
||||||
|
0xfe, 0x04, 0x08, 0x30, 0x40, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
// O (79)
|
||||||
|
0x7c, 0x82, 0x82, 0x82, 0x82, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
// P (80)
|
||||||
|
0xfe, 0x12, 0x12, 0x12, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// Q (81)
|
||||||
|
0x7c, 0x82, 0x82, 0x82, 0x42, 0xc2, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
// R (82)
|
||||||
|
0xfe, 0x12, 0x12, 0x32, 0x4c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
// S (83)
|
||||||
|
0x4c, 0x92, 0x92, 0x92, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// T (84)
|
||||||
|
0x02, 0x02, 0xfe, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// U (85)
|
||||||
|
0x7e, 0x80, 0x80, 0x80, 0x80, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
// V (86)
|
||||||
|
0x06, 0x18, 0x60, 0x80, 0x60, 0x18, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
// W (87)
|
||||||
|
0x06, 0x38, 0xc0, 0x38, 0x06, 0x38, 0xc0, 0x38, 0x06, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// X (88)
|
||||||
|
0x82, 0x44, 0x28, 0x10, 0x28, 0x44, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
// Y (89)
|
||||||
|
0x02, 0x04, 0x08, 0xf0, 0x08, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
// Z (90)
|
||||||
|
0xc2, 0xa2, 0x92, 0x8a, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// [ (91)
|
||||||
|
0x00, 0xfe, 0x82, 0x82, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// \ (92)
|
||||||
|
0x06, 0x38, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// ] (93)
|
||||||
|
0x82, 0x82, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// ^ (94)
|
||||||
|
0x08, 0x04, 0x02, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// _ (95)
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// ` (96)
|
||||||
|
0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// a (97)
|
||||||
|
0x40, 0xa8, 0xa8, 0xa8, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// b (98)
|
||||||
|
0xfe, 0x88, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// c (99)
|
||||||
|
0x70, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// d (100)
|
||||||
|
0x70, 0x88, 0x88, 0x88, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// e (101)
|
||||||
|
0x70, 0xa8, 0xa8, 0xa8, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// f (102)
|
||||||
|
0x08, 0xfc, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// g (103)
|
||||||
|
0x30, 0x48, 0x48, 0x48, 0xf8, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00,
|
||||||
|
// h (104)
|
||||||
|
0xfe, 0x08, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// i (105)
|
||||||
|
0x00, 0xfa, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// j (106)
|
||||||
|
0x00, 0x00, 0xfa, 0x01, 0x00, 0x00,
|
||||||
|
// k (107)
|
||||||
|
0xfe, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// l (108)
|
||||||
|
0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// m (109)
|
||||||
|
0xf8, 0x08, 0x08, 0xf0, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// n (110)
|
||||||
|
0xf8, 0x08, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// o (111)
|
||||||
|
0x70, 0x88, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// p (112)
|
||||||
|
0xf8, 0x48, 0x48, 0x48, 0x30, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// q (113)
|
||||||
|
0x30, 0x48, 0x48, 0x48, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
||||||
|
// r (114)
|
||||||
|
0xf8, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// s (115)
|
||||||
|
0x90, 0xa8, 0xa8, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// t (116)
|
||||||
|
0x08, 0x7e, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// u (117)
|
||||||
|
0x78, 0x80, 0x80, 0x80, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// v (118)
|
||||||
|
0x18, 0x60, 0x80, 0x60, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// w (119)
|
||||||
|
0x78, 0x80, 0x60, 0x18, 0x60, 0x80, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// x (120)
|
||||||
|
0x88, 0x50, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// y (121)
|
||||||
|
0x18, 0x60, 0x80, 0x60, 0x18, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// z (122)
|
||||||
|
0xc8, 0xa8, 0x98, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// { (123)
|
||||||
|
0x10, 0x6c, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// | (124)
|
||||||
|
0x00, 0xfe, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// } (125)
|
||||||
|
0x82, 0x6c, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// ~ (126)
|
||||||
|
0x10, 0x08, 0x10, 0x20, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ArialMT Plain 16 - Medium font (height ~19 pixels)
|
||||||
|
* Simplified subset covering 0-9, A-Z, a-z, common punctuation
|
||||||
|
*/
|
||||||
|
export const ArialMT_Plain_16: OLEDFont = {
|
||||||
|
height: 19,
|
||||||
|
firstChar: 32,
|
||||||
|
charCount: 95,
|
||||||
|
widths: [
|
||||||
|
4, // space
|
||||||
|
4, // !
|
||||||
|
5, // "
|
||||||
|
8, // #
|
||||||
|
8, // $
|
||||||
|
12, // %
|
||||||
|
10, // &
|
||||||
|
2, // '
|
||||||
|
5, // (
|
||||||
|
5, // )
|
||||||
|
6, // *
|
||||||
|
8, // +
|
||||||
|
4, // ,
|
||||||
|
5, // -
|
||||||
|
4, // .
|
||||||
|
4, // /
|
||||||
|
8, // 0
|
||||||
|
8, // 1
|
||||||
|
8, // 2
|
||||||
|
8, // 3
|
||||||
|
8, // 4
|
||||||
|
8, // 5
|
||||||
|
8, // 6
|
||||||
|
8, // 7
|
||||||
|
8, // 8
|
||||||
|
8, // 9
|
||||||
|
4, // :
|
||||||
|
4, // ;
|
||||||
|
8, // <
|
||||||
|
8, // =
|
||||||
|
8, // >
|
||||||
|
8, // ?
|
||||||
|
15, // @
|
||||||
|
10, // A
|
||||||
|
9, // B
|
||||||
|
10, // C
|
||||||
|
10, // D
|
||||||
|
9, // E
|
||||||
|
8, // F
|
||||||
|
11, // G
|
||||||
|
9, // H
|
||||||
|
4, // I
|
||||||
|
6, // J
|
||||||
|
9, // K
|
||||||
|
8, // L
|
||||||
|
11, // M
|
||||||
|
9, // N
|
||||||
|
11, // O
|
||||||
|
9, // P
|
||||||
|
11, // Q
|
||||||
|
10, // R
|
||||||
|
9, // S
|
||||||
|
8, // T
|
||||||
|
9, // U
|
||||||
|
10, // V
|
||||||
|
14, // W
|
||||||
|
10, // X
|
||||||
|
10, // Y
|
||||||
|
9, // Z
|
||||||
|
4, // [
|
||||||
|
4, // \
|
||||||
|
4, // ]
|
||||||
|
7, // ^
|
||||||
|
8, // _
|
||||||
|
5, // `
|
||||||
|
8, // a
|
||||||
|
8, // b
|
||||||
|
7, // c
|
||||||
|
8, // d
|
||||||
|
8, // e
|
||||||
|
4, // f
|
||||||
|
8, // g
|
||||||
|
8, // h
|
||||||
|
4, // i
|
||||||
|
4, // j
|
||||||
|
8, // k
|
||||||
|
4, // l
|
||||||
|
12, // m
|
||||||
|
8, // n
|
||||||
|
8, // o
|
||||||
|
8, // p
|
||||||
|
8, // q
|
||||||
|
5, // r
|
||||||
|
7, // s
|
||||||
|
4, // t
|
||||||
|
8, // u
|
||||||
|
8, // v
|
||||||
|
12, // w
|
||||||
|
8, // x
|
||||||
|
8, // y
|
||||||
|
7, // z
|
||||||
|
5, // {
|
||||||
|
4, // |
|
||||||
|
5, // }
|
||||||
|
8, // ~
|
||||||
|
],
|
||||||
|
// Font data would be similar pattern but larger - using placeholder
|
||||||
|
data: new Uint8Array(2500).fill(0xff), // Placeholder - would contain actual font bitmap data
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get FONT_HEIGHT_SMALL equivalent
|
||||||
|
*/
|
||||||
|
export const FONT_HEIGHT_SMALL = 13;
|
||||||
|
export const FONT_HEIGHT_MEDIUM = 19;
|
||||||
|
export const FONT_HEIGHT_LARGE = 28;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to create a font from raw byte data (for importing firmware fonts)
|
||||||
|
*/
|
||||||
|
export function createFontFromBytes(
|
||||||
|
height: number,
|
||||||
|
firstChar: number,
|
||||||
|
widths: number[],
|
||||||
|
data: number[],
|
||||||
|
): OLEDFont {
|
||||||
|
return {
|
||||||
|
height,
|
||||||
|
firstChar,
|
||||||
|
charCount: widths.length,
|
||||||
|
widths,
|
||||||
|
data: new Uint8Array(data),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
/**
|
||||||
|
* OLEDImages.ts - Icon and image data for OLED display emulator
|
||||||
|
*
|
||||||
|
* These are direct ports of the XBM/bitmap icons from the Meshtastic firmware.
|
||||||
|
* Format: XBM style (LSB first, horizontal scanning)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Satellite icon (8x8)
|
||||||
|
export const imgSatellite_width = 8;
|
||||||
|
export const imgSatellite_height = 8;
|
||||||
|
export const imgSatellite = new Uint8Array([
|
||||||
|
0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b11011011, 0b11111111,
|
||||||
|
0b11011011, 0b00011000,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// USB icon (10x8)
|
||||||
|
export const imgUSB = new Uint8Array([
|
||||||
|
0x00, 0xfc, 0xf0, 0xfc, 0x88, 0xff, 0x86, 0xfe, 0x85, 0xfe, 0x89, 0xff, 0xf1,
|
||||||
|
0xfc, 0x00, 0xfc,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Power icon (8x16)
|
||||||
|
export const imgPower = new Uint8Array([
|
||||||
|
0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, 0x1c, 0x22, 0x22, 0x41, 0x7f,
|
||||||
|
0x22, 0x22, 0x22,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// User icon (8x8)
|
||||||
|
export const imgUser = new Uint8Array([
|
||||||
|
0x3c, 0x42, 0x99, 0xa5, 0xa5, 0x99, 0x42, 0x3c,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Position empty arrow (6x8)
|
||||||
|
export const imgPositionEmpty = new Uint8Array([
|
||||||
|
0x20, 0x30, 0x28, 0x24, 0x42, 0xff,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Position solid arrow (6x8)
|
||||||
|
export const imgPositionSolid = new Uint8Array([
|
||||||
|
0x20, 0x30, 0x38, 0x3c, 0x7e, 0xff,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mail icon (10x7)
|
||||||
|
export const mail_width = 10;
|
||||||
|
export const mail_height = 7;
|
||||||
|
export const mail = new Uint8Array([
|
||||||
|
0b11111111,
|
||||||
|
0b00, // Top line
|
||||||
|
0b10000001,
|
||||||
|
0b00, // Edges
|
||||||
|
0b11000011,
|
||||||
|
0b00, // Diagonals start
|
||||||
|
0b10100101,
|
||||||
|
0b00, // Inner M part
|
||||||
|
0b10011001,
|
||||||
|
0b00, // Inner M part
|
||||||
|
0b10000001,
|
||||||
|
0b00, // Edges
|
||||||
|
0b11111111,
|
||||||
|
0b00, // Bottom line
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Hop icon (9x10)
|
||||||
|
export const hop_width = 9;
|
||||||
|
export const hop_height = 10;
|
||||||
|
export const hop = new Uint8Array([
|
||||||
|
0x05, 0x00, 0x07, 0x00, 0x05, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, 0x00, 0xc0,
|
||||||
|
0x01, 0x40, 0x01, 0xc0, 0x01, 0x40, 0x00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mail icon (8x8)
|
||||||
|
export const icon_mail = new Uint8Array([
|
||||||
|
0b11111111, // ████████ top border
|
||||||
|
0b10000001, // █ █ sides
|
||||||
|
0b11000011, // ██ ██ diagonal
|
||||||
|
0b10100101, // █ █ █ █ inner M
|
||||||
|
0b10011001, // █ ██ █ inner M
|
||||||
|
0b10000001, // █ █ sides
|
||||||
|
0b10000001, // █ █ sides
|
||||||
|
0b11111111, // ████████ bottom
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Compass icon (8x8)
|
||||||
|
export const icon_compass = new Uint8Array([
|
||||||
|
0x3c, // Row 0: ..####..
|
||||||
|
0x52, // Row 1: .#..#.#.
|
||||||
|
0x91, // Row 2: #...#..#
|
||||||
|
0x91, // Row 3: #...#..#
|
||||||
|
0x91, // Row 4: #...#..#
|
||||||
|
0x81, // Row 5: #......#
|
||||||
|
0x42, // Row 6: .#....#.
|
||||||
|
0x3c, // Row 7: ..####..
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Radio icon (8x8)
|
||||||
|
export const icon_radio = new Uint8Array([
|
||||||
|
0x0f, // Row 0: ####....
|
||||||
|
0x10, // Row 1: ....#...
|
||||||
|
0x27, // Row 2: ###..#..
|
||||||
|
0x48, // Row 3: ...#..#.
|
||||||
|
0x93, // Row 4: ##..#..#
|
||||||
|
0xa4, // Row 5: ..#..#.#
|
||||||
|
0xa8, // Row 6: ...#.#.#
|
||||||
|
0xa9, // Row 7: #..#.#.#
|
||||||
|
]);
|
||||||
|
|
||||||
|
// System/settings icon (8x8)
|
||||||
|
export const icon_system = new Uint8Array([
|
||||||
|
0x24, // Row 0: ..#..#..
|
||||||
|
0x3c, // Row 1: ..####..
|
||||||
|
0xc3, // Row 2: ##....##
|
||||||
|
0x5a, // Row 3: .#.##.#.
|
||||||
|
0x5a, // Row 4: .#.##.#.
|
||||||
|
0xc3, // Row 5: ##....##
|
||||||
|
0x3c, // Row 6: ..####..
|
||||||
|
0x24, // Row 7: ..#..#..
|
||||||
|
]);
|
||||||
|
|
||||||
|
// WiFi icon (8x8)
|
||||||
|
export const icon_wifi = new Uint8Array([
|
||||||
|
0b00000000, 0b00011000, 0b00111100, 0b01111110, 0b11011011, 0b00011000,
|
||||||
|
0b00011000, 0b00000000,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Nodes icon (8x8)
|
||||||
|
export const icon_nodes = new Uint8Array([
|
||||||
|
0xf9, // Row 0: #..#####
|
||||||
|
0x00, // Row 1
|
||||||
|
0xf9, // Row 2: #..#####
|
||||||
|
0x00, // Row 3
|
||||||
|
0xf9, // Row 4: #..#####
|
||||||
|
0x00, // Row 5
|
||||||
|
0xf9, // Row 6: #..#####
|
||||||
|
0x00, // Row 7
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Battery vertical (7x11)
|
||||||
|
export const batteryBitmap_v = new Uint8Array([
|
||||||
|
0b00011100, 0b00111110, 0b01000001, 0b01000001, 0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b01000001, 0b01000001, 0b01000001, 0b00111110,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Battery side gaps (8x3)
|
||||||
|
export const batteryBitmap_sidegaps_v = new Uint8Array([
|
||||||
|
0b10000010, 0b10000010, 0b10000010,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Lightning bolt vertical (5x5)
|
||||||
|
export const lightning_bolt_v = new Uint8Array([
|
||||||
|
0b00000100, 0b00000110, 0b00011111, 0b00001100, 0b00000100,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Horizontal battery bottom (13x9)
|
||||||
|
export const batteryBitmap_h_bottom = new Uint8Array([
|
||||||
|
0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
||||||
|
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
||||||
|
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
||||||
|
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
||||||
|
0b00011110, 0b00000000,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Horizontal battery top (13x9)
|
||||||
|
export const batteryBitmap_h_top = new Uint8Array([
|
||||||
|
0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000,
|
||||||
|
0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b11000000, 0b00000000,
|
||||||
|
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000,
|
||||||
|
0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000,
|
||||||
|
0b00111100, 0b00000000,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Lightning bolt horizontal (13x9)
|
||||||
|
export const lightning_bolt_h = new Uint8Array([
|
||||||
|
0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000,
|
||||||
|
0b00111000, 0b00000000, 0b00111100, 0b00000000, 0b00011110, 0b00000000,
|
||||||
|
0b11111111, 0b00000000, 0b01111000, 0b00000000, 0b00111100, 0b00000000,
|
||||||
|
0b00011100, 0b00000000, 0b00001100, 0b00000000, 0b00000100, 0b00000000,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Signal bars icon (various signal strengths)
|
||||||
|
export const signal_bars = {
|
||||||
|
width: 12,
|
||||||
|
height: 8,
|
||||||
|
// 0 bars
|
||||||
|
none: new Uint8Array([
|
||||||
|
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||||
|
]),
|
||||||
|
// 1 bar
|
||||||
|
low: new Uint8Array([
|
||||||
|
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||||
|
0b00000011, 0b00000000, 0b00000011, 0b00000000,
|
||||||
|
]),
|
||||||
|
// 2 bars
|
||||||
|
medium: new Uint8Array([
|
||||||
|
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000, 0b00001100, 0b00000000, 0b00001100, 0b00000000,
|
||||||
|
0b00001111, 0b00000000, 0b00001111, 0b00000000,
|
||||||
|
]),
|
||||||
|
// 3 bars
|
||||||
|
high: new Uint8Array([
|
||||||
|
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00110000, 0b00000000,
|
||||||
|
0b00110000, 0b00000000, 0b00111100, 0b00000000, 0b00111100, 0b00000000,
|
||||||
|
0b00111111, 0b00000000, 0b00111111, 0b00000000,
|
||||||
|
]),
|
||||||
|
// 4 bars (full)
|
||||||
|
full: new Uint8Array([
|
||||||
|
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11110000, 0b00000000,
|
||||||
|
0b11110000, 0b00000000, 0b11111100, 0b00000000, 0b11111100, 0b00000000,
|
||||||
|
0b11111111, 0b00000000, 0b11111111, 0b00000000,
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Meshtastic logo (simplified 32x32)
|
||||||
|
export const logo_width = 32;
|
||||||
|
export const logo_height = 32;
|
||||||
|
export const meshtastic_logo = new Uint8Array([
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00,
|
||||||
|
0x03, 0xc0, 0x00, 0x80, 0x01, 0x80, 0x01, 0xc0, 0x00, 0x00, 0x03, 0x60, 0x00,
|
||||||
|
0x00, 0x06, 0x30, 0x00, 0x00, 0x0c, 0x18, 0xf8, 0x1f, 0x18, 0x08, 0x04, 0x20,
|
||||||
|
0x10, 0x0c, 0x02, 0x40, 0x30, 0x04, 0x01, 0x80, 0x20, 0x06, 0x01, 0x80, 0x60,
|
||||||
|
0x02, 0xe1, 0x87, 0x40, 0x02, 0x11, 0x88, 0x40, 0x03, 0x09, 0x90, 0xc0, 0x03,
|
||||||
|
0x09, 0x90, 0xc0, 0x02, 0x11, 0x88, 0x40, 0x02, 0xe1, 0x87, 0x40, 0x06, 0x01,
|
||||||
|
0x80, 0x60, 0x04, 0x01, 0x80, 0x20, 0x0c, 0x02, 0x40, 0x30, 0x08, 0x04, 0x20,
|
||||||
|
0x10, 0x18, 0xf8, 0x1f, 0x18, 0x30, 0x00, 0x00, 0x0c, 0x60, 0x00, 0x00, 0x06,
|
||||||
|
0xc0, 0x00, 0x00, 0x03, 0x80, 0x01, 0x80, 0x01, 0x00, 0x03, 0xc0, 0x00, 0x00,
|
||||||
|
0x06, 0x60, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Bluetooth icon (8x11)
|
||||||
|
export const icon_bluetooth = new Uint8Array([
|
||||||
|
0b00000000, 0b10000100, 0b01001010, 0b00101010, 0b00010100, 0b11111111,
|
||||||
|
0b00010100, 0b00101010, 0b01001010, 0b10000100, 0b00000000,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// GPS/Location pin icon (8x12)
|
||||||
|
export const icon_gps_pin = new Uint8Array([
|
||||||
|
0b00111100, 0b01111110, 0b11111111, 0b11100111, 0b11100111, 0b11111111,
|
||||||
|
0b01111110, 0b00111100, 0b00011000, 0b00011000, 0b00001000, 0b00000000,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Message/chat bubble icon (10x8)
|
||||||
|
export const icon_message = new Uint8Array([
|
||||||
|
0b11111111, 0b00, 0b10000001, 0b00, 0b10000001, 0b00, 0b10000001, 0b00,
|
||||||
|
0b10000001, 0b00, 0b11111111, 0b00, 0b00000110, 0b00, 0b00000010, 0b00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to convert icon data to XBM format if needed
|
||||||
|
*/
|
||||||
|
export function toXbm(data: Uint8Array | number[]): Uint8Array {
|
||||||
|
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
||||||
|
}
|
||||||
@@ -0,0 +1,627 @@
|
|||||||
|
/**
|
||||||
|
* ScreenRenderers.ts - Sample screen rendering functions
|
||||||
|
*
|
||||||
|
* These demonstrate how to port Meshtastic firmware UI screens
|
||||||
|
* to the OLED emulator. The API matches the firmware's drawing code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
OLEDDisplay,
|
||||||
|
WHITE,
|
||||||
|
BLACK,
|
||||||
|
TEXT_ALIGN_LEFT,
|
||||||
|
TEXT_ALIGN_CENTER,
|
||||||
|
TEXT_ALIGN_RIGHT,
|
||||||
|
} from "./OLEDDisplay";
|
||||||
|
import { Font_6x8 } from "./SimpleFonts";
|
||||||
|
import * as images from "./OLEDImages";
|
||||||
|
|
||||||
|
// Font height for simple 6x8 font
|
||||||
|
const FONT_HEIGHT_SMALL = 8;
|
||||||
|
|
||||||
|
// Screen resolution detection (matches SharedUIDisplay.cpp)
|
||||||
|
export type ScreenResolution = "UltraLow" | "Low" | "High";
|
||||||
|
|
||||||
|
export function determineScreenResolution(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
): ScreenResolution {
|
||||||
|
if (width <= 64 || height <= 48) {
|
||||||
|
return "UltraLow";
|
||||||
|
}
|
||||||
|
if (width > 128) {
|
||||||
|
return "High";
|
||||||
|
}
|
||||||
|
return "Low";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper for consistent line positioning
|
||||||
|
export function getTextPositions(display: OLEDDisplay): number[] {
|
||||||
|
const fontHeight = FONT_HEIGHT_SMALL;
|
||||||
|
return [
|
||||||
|
0, // textZeroLine
|
||||||
|
fontHeight - 1, // textFirstLine
|
||||||
|
fontHeight - 1 + (fontHeight - 5), // textSecondLine
|
||||||
|
fontHeight - 1 + 2 * (fontHeight - 5), // textThirdLine
|
||||||
|
fontHeight - 1 + 3 * (fontHeight - 5), // textFourthLine
|
||||||
|
fontHeight - 1 + 4 * (fontHeight - 5), // textFifthLine
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a rounded highlight box (used for inverted headers)
|
||||||
|
*/
|
||||||
|
export function drawRoundedHighlight(
|
||||||
|
display: OLEDDisplay,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
r: number,
|
||||||
|
): void {
|
||||||
|
// Draw the center and side rectangles
|
||||||
|
display.fillRect(x + r, y, w - 2 * r, h); // center bar
|
||||||
|
display.fillRect(x, y + r, r, h - 2 * r); // left edge
|
||||||
|
display.fillRect(x + w - r, y + r, r, h - 2 * r); // right edge
|
||||||
|
|
||||||
|
// Draw the rounded corners using filled circles
|
||||||
|
display.fillCircle(x + r + 1, y + r, r); // top-left
|
||||||
|
display.fillCircle(x + w - r - 1, y + r, r); // top-right
|
||||||
|
display.fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left
|
||||||
|
display.fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw the common header bar (battery, title, icons)
|
||||||
|
*/
|
||||||
|
export function drawCommonHeader(
|
||||||
|
display: OLEDDisplay,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
titleStr: string,
|
||||||
|
options: {
|
||||||
|
inverted?: boolean;
|
||||||
|
batteryPercent?: number;
|
||||||
|
isCharging?: boolean;
|
||||||
|
hasUSB?: boolean;
|
||||||
|
hasUnreadMessage?: boolean;
|
||||||
|
} = {},
|
||||||
|
): void {
|
||||||
|
const {
|
||||||
|
inverted = true,
|
||||||
|
batteryPercent = 75,
|
||||||
|
isCharging = false,
|
||||||
|
hasUSB = false,
|
||||||
|
hasUnreadMessage = false,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const HEADER_OFFSET_Y = 1;
|
||||||
|
y += HEADER_OFFSET_Y;
|
||||||
|
|
||||||
|
display.setFont(Font_6x8);
|
||||||
|
const highlightHeight = FONT_HEIGHT_SMALL - 1;
|
||||||
|
const screenW = display.getWidth();
|
||||||
|
|
||||||
|
if (inverted) {
|
||||||
|
// Draw inverted header background
|
||||||
|
display.setColor(WHITE);
|
||||||
|
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
||||||
|
display.setColor(BLACK);
|
||||||
|
} else {
|
||||||
|
// Draw line under header
|
||||||
|
display.setColor(WHITE);
|
||||||
|
display.drawLine(0, 14, screenW, 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw title centered
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
|
display.drawString(screenW / 2, y, titleStr);
|
||||||
|
|
||||||
|
// Reset color for remaining elements
|
||||||
|
if (inverted) {
|
||||||
|
display.setColor(WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw battery on left
|
||||||
|
let batteryX = 1;
|
||||||
|
const batteryY = HEADER_OFFSET_Y + 1;
|
||||||
|
|
||||||
|
if (hasUSB && !isCharging) {
|
||||||
|
// Draw USB icon
|
||||||
|
display.drawXbm(batteryX + 1, batteryY + 2, 10, 8, images.imgUSB);
|
||||||
|
batteryX += 11;
|
||||||
|
} else {
|
||||||
|
// Draw battery outline
|
||||||
|
display.drawXbm(batteryX, batteryY, 7, 11, images.batteryBitmap_v);
|
||||||
|
|
||||||
|
if (isCharging) {
|
||||||
|
// Draw lightning bolt
|
||||||
|
display.drawXbm(
|
||||||
|
batteryX + 1,
|
||||||
|
batteryY + 3,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
images.lightning_bolt_v,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Draw battery level
|
||||||
|
display.drawXbm(
|
||||||
|
batteryX - 1,
|
||||||
|
batteryY + 4,
|
||||||
|
8,
|
||||||
|
3,
|
||||||
|
images.batteryBitmap_sidegaps_v,
|
||||||
|
);
|
||||||
|
const fillHeight = Math.floor((8 * batteryPercent) / 100);
|
||||||
|
const fillY = batteryY - fillHeight + 10;
|
||||||
|
display.fillRect(batteryX + 1, fillY, 5, fillHeight);
|
||||||
|
}
|
||||||
|
batteryX += 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw mail icon on right if unread message
|
||||||
|
if (hasUnreadMessage) {
|
||||||
|
const mailX = screenW - images.mail_width - 2;
|
||||||
|
display.drawXbm(
|
||||||
|
mailX,
|
||||||
|
y + 2,
|
||||||
|
images.mail_width,
|
||||||
|
images.mail_height,
|
||||||
|
images.mail,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display.setColor(WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a boot/splash screen with logo
|
||||||
|
*/
|
||||||
|
export function drawBootScreen(
|
||||||
|
display: OLEDDisplay,
|
||||||
|
appVersion: string = "2.5.0",
|
||||||
|
): void {
|
||||||
|
display.clear();
|
||||||
|
display.setFont(Font_6x8);
|
||||||
|
display.setColor(WHITE);
|
||||||
|
|
||||||
|
const centerX = display.getWidth() / 2;
|
||||||
|
const centerY = display.getHeight() / 2;
|
||||||
|
|
||||||
|
// Draw logo centered
|
||||||
|
const logoX = centerX - images.logo_width / 2;
|
||||||
|
const logoY = centerY - images.logo_height / 2 - 8;
|
||||||
|
display.drawXbm(
|
||||||
|
logoX,
|
||||||
|
logoY,
|
||||||
|
images.logo_width,
|
||||||
|
images.logo_height,
|
||||||
|
images.meshtastic_logo,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw version below
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
|
display.drawString(
|
||||||
|
centerX,
|
||||||
|
centerY + images.logo_height / 2,
|
||||||
|
`v${appVersion}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a node info screen
|
||||||
|
*/
|
||||||
|
export function drawNodeInfoScreen(
|
||||||
|
display: OLEDDisplay,
|
||||||
|
nodeInfo: {
|
||||||
|
shortName: string;
|
||||||
|
longName: string;
|
||||||
|
nodeId: string;
|
||||||
|
batteryLevel?: number;
|
||||||
|
lastHeard?: string;
|
||||||
|
snr?: number;
|
||||||
|
hopsAway?: number;
|
||||||
|
},
|
||||||
|
): void {
|
||||||
|
display.clear();
|
||||||
|
|
||||||
|
// Draw header
|
||||||
|
drawCommonHeader(display, 0, 0, "Node Info", {
|
||||||
|
inverted: true,
|
||||||
|
batteryPercent: 85,
|
||||||
|
});
|
||||||
|
|
||||||
|
display.setFont(Font_6x8);
|
||||||
|
display.setColor(WHITE);
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
|
const lines = getTextPositions(display);
|
||||||
|
const screenW = display.getWidth();
|
||||||
|
|
||||||
|
// Node name (bolded by drawing twice offset)
|
||||||
|
display.drawString(0, lines[1], nodeInfo.longName);
|
||||||
|
display.drawString(1, lines[1], nodeInfo.longName);
|
||||||
|
|
||||||
|
// Short name and ID
|
||||||
|
display.drawString(0, lines[2], `${nodeInfo.shortName} • ${nodeInfo.nodeId}`);
|
||||||
|
|
||||||
|
// Last heard
|
||||||
|
if (nodeInfo.lastHeard) {
|
||||||
|
display.drawString(0, lines[3], `Heard: ${nodeInfo.lastHeard}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal info on right side
|
||||||
|
if (nodeInfo.snr !== undefined) {
|
||||||
|
const snrStr = `SNR: ${nodeInfo.snr}dB`;
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||||
|
display.drawString(screenW, lines[3], snrStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hops
|
||||||
|
if (nodeInfo.hopsAway !== undefined) {
|
||||||
|
const hopStr =
|
||||||
|
nodeInfo.hopsAway === 0 ? "Direct" : `${nodeInfo.hopsAway} hops`;
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display.drawString(0, lines[4], hopStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a message screen
|
||||||
|
*/
|
||||||
|
export function drawMessageScreen(
|
||||||
|
display: OLEDDisplay,
|
||||||
|
message: {
|
||||||
|
from: string;
|
||||||
|
text: string;
|
||||||
|
time: string;
|
||||||
|
channel?: string;
|
||||||
|
},
|
||||||
|
): void {
|
||||||
|
display.clear();
|
||||||
|
|
||||||
|
// Draw header with channel name if provided
|
||||||
|
const headerTitle = message.channel || "Message";
|
||||||
|
drawCommonHeader(display, 0, 0, headerTitle, {
|
||||||
|
inverted: true,
|
||||||
|
batteryPercent: 72,
|
||||||
|
hasUnreadMessage: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
display.setFont(Font_6x8);
|
||||||
|
display.setColor(WHITE);
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
|
const lines = getTextPositions(display);
|
||||||
|
const screenW = display.getWidth();
|
||||||
|
|
||||||
|
// From line
|
||||||
|
display.drawString(0, lines[1], `From: ${message.from}`);
|
||||||
|
|
||||||
|
// Time on right
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||||
|
display.drawString(screenW, lines[1], message.time);
|
||||||
|
|
||||||
|
// Message text (with word wrap)
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display.drawStringMaxWidth(0, lines[2], screenW, message.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a GPS/location screen
|
||||||
|
*/
|
||||||
|
export function drawGPSScreen(
|
||||||
|
display: OLEDDisplay,
|
||||||
|
gpsInfo: {
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
altitude?: number;
|
||||||
|
satellites?: number;
|
||||||
|
hasLock: boolean;
|
||||||
|
speed?: number;
|
||||||
|
},
|
||||||
|
): void {
|
||||||
|
display.clear();
|
||||||
|
|
||||||
|
drawCommonHeader(display, 0, 0, "GPS", {
|
||||||
|
inverted: true,
|
||||||
|
batteryPercent: 90,
|
||||||
|
});
|
||||||
|
|
||||||
|
display.setFont(Font_6x8);
|
||||||
|
display.setColor(WHITE);
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
|
const lines = getTextPositions(display);
|
||||||
|
|
||||||
|
// Draw satellite icon and count
|
||||||
|
display.drawXbm(
|
||||||
|
0,
|
||||||
|
lines[1],
|
||||||
|
images.imgSatellite_width,
|
||||||
|
images.imgSatellite_height,
|
||||||
|
images.imgSatellite,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!gpsInfo.hasLock) {
|
||||||
|
display.drawString(12, lines[1], "No Lock");
|
||||||
|
} else {
|
||||||
|
display.drawString(12, lines[1], `${gpsInfo.satellites || 0} sats`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
gpsInfo.hasLock &&
|
||||||
|
gpsInfo.latitude !== undefined &&
|
||||||
|
gpsInfo.longitude !== undefined
|
||||||
|
) {
|
||||||
|
// Coordinates
|
||||||
|
display.drawString(0, lines[2], `Lat: ${gpsInfo.latitude.toFixed(6)}`);
|
||||||
|
display.drawString(0, lines[3], `Lon: ${gpsInfo.longitude.toFixed(6)}`);
|
||||||
|
|
||||||
|
// Altitude
|
||||||
|
if (gpsInfo.altitude !== undefined) {
|
||||||
|
display.drawString(0, lines[4], `Alt: ${gpsInfo.altitude.toFixed(0)}m`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speed
|
||||||
|
if (gpsInfo.speed !== undefined) {
|
||||||
|
const screenW = display.getWidth();
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||||
|
display.drawString(screenW, lines[4], `${gpsInfo.speed.toFixed(1)} km/h`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a node list screen
|
||||||
|
*/
|
||||||
|
export function drawNodeListScreen(
|
||||||
|
display: OLEDDisplay,
|
||||||
|
nodes: Array<{
|
||||||
|
shortName: string;
|
||||||
|
longName: string;
|
||||||
|
lastHeard: string;
|
||||||
|
snr?: number;
|
||||||
|
}>,
|
||||||
|
selectedIndex: number = 0,
|
||||||
|
): void {
|
||||||
|
display.clear();
|
||||||
|
|
||||||
|
drawCommonHeader(display, 0, 0, `Nodes (${nodes.length})`, {
|
||||||
|
inverted: true,
|
||||||
|
batteryPercent: 80,
|
||||||
|
});
|
||||||
|
|
||||||
|
display.setFont(Font_6x8);
|
||||||
|
display.setColor(WHITE);
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
|
const lines = getTextPositions(display);
|
||||||
|
const screenW = display.getWidth();
|
||||||
|
const maxVisibleNodes = 4;
|
||||||
|
const startIndex = Math.max(
|
||||||
|
0,
|
||||||
|
selectedIndex - Math.floor(maxVisibleNodes / 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < maxVisibleNodes && startIndex + i < nodes.length; i++) {
|
||||||
|
const node = nodes[startIndex + i];
|
||||||
|
const lineY = lines[i + 1];
|
||||||
|
const isSelected = startIndex + i === selectedIndex;
|
||||||
|
|
||||||
|
// Highlight selected row
|
||||||
|
if (isSelected) {
|
||||||
|
display.fillRect(0, lineY - 1, screenW, FONT_HEIGHT_SMALL);
|
||||||
|
display.setColor(BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node short name
|
||||||
|
display.drawString(0, lineY, node.shortName);
|
||||||
|
|
||||||
|
// Last heard on right
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||||
|
display.drawString(screenW, lineY, node.lastHeard);
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
display.setColor(WHITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a system info screen
|
||||||
|
*/
|
||||||
|
export function drawSystemScreen(
|
||||||
|
display: OLEDDisplay,
|
||||||
|
sysInfo: {
|
||||||
|
uptime: string;
|
||||||
|
channelUtil: number;
|
||||||
|
airUtil: number;
|
||||||
|
batteryVoltage?: number;
|
||||||
|
nodes: number;
|
||||||
|
freeMemory?: number;
|
||||||
|
},
|
||||||
|
): void {
|
||||||
|
display.clear();
|
||||||
|
|
||||||
|
drawCommonHeader(display, 0, 0, "System", {
|
||||||
|
inverted: true,
|
||||||
|
batteryPercent: 95,
|
||||||
|
});
|
||||||
|
|
||||||
|
display.setFont(Font_6x8);
|
||||||
|
display.setColor(WHITE);
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
|
const lines = getTextPositions(display);
|
||||||
|
const screenW = display.getWidth();
|
||||||
|
|
||||||
|
// Uptime
|
||||||
|
display.drawString(0, lines[1], `Uptime: ${sysInfo.uptime}`);
|
||||||
|
|
||||||
|
// Channel utilization
|
||||||
|
display.drawString(0, lines[2], `ChUtil: ${sysInfo.channelUtil.toFixed(1)}%`);
|
||||||
|
|
||||||
|
// Air utilization
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||||
|
display.drawString(
|
||||||
|
screenW,
|
||||||
|
lines[2],
|
||||||
|
`AirTx: ${sysInfo.airUtil.toFixed(1)}%`,
|
||||||
|
);
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
|
// Nodes
|
||||||
|
display.drawString(0, lines[3], `Nodes: ${sysInfo.nodes}`);
|
||||||
|
|
||||||
|
// Battery voltage
|
||||||
|
if (sysInfo.batteryVoltage !== undefined) {
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||||
|
display.drawString(
|
||||||
|
screenW,
|
||||||
|
lines[3],
|
||||||
|
`${sysInfo.batteryVoltage.toFixed(2)}V`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free memory
|
||||||
|
if (sysInfo.freeMemory !== undefined) {
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display.drawString(
|
||||||
|
0,
|
||||||
|
lines[4],
|
||||||
|
`Free: ${(sysInfo.freeMemory / 1024).toFixed(0)}KB`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a compass screen with bearing arrow
|
||||||
|
*/
|
||||||
|
export function drawCompassScreen(
|
||||||
|
display: OLEDDisplay,
|
||||||
|
compassInfo: {
|
||||||
|
heading: number; // Device heading in degrees
|
||||||
|
bearing: number; // Bearing to target in degrees
|
||||||
|
distance?: number; // Distance to target in meters
|
||||||
|
targetName?: string;
|
||||||
|
},
|
||||||
|
): void {
|
||||||
|
display.clear();
|
||||||
|
|
||||||
|
drawCommonHeader(display, 0, 0, compassInfo.targetName || "Compass", {
|
||||||
|
inverted: true,
|
||||||
|
batteryPercent: 70,
|
||||||
|
});
|
||||||
|
|
||||||
|
display.setFont(Font_6x8);
|
||||||
|
display.setColor(WHITE);
|
||||||
|
|
||||||
|
const screenW = display.getWidth();
|
||||||
|
const screenH = display.getHeight();
|
||||||
|
|
||||||
|
// Compass center and radius
|
||||||
|
const compassRadius = Math.min(screenW, screenH - 20) / 2 - 4;
|
||||||
|
const compassX = screenW / 2;
|
||||||
|
const compassY = (screenH + 16) / 2;
|
||||||
|
|
||||||
|
// Draw compass circle
|
||||||
|
display.drawCircle(compassX, compassY, compassRadius);
|
||||||
|
|
||||||
|
// Draw cardinal directions
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
|
display.drawString(compassX, compassY - compassRadius - 10, "N");
|
||||||
|
|
||||||
|
// Calculate relative bearing (bearing - heading)
|
||||||
|
const relativeBearing =
|
||||||
|
((compassInfo.bearing - compassInfo.heading) * Math.PI) / 180;
|
||||||
|
|
||||||
|
// Draw bearing arrow
|
||||||
|
const arrowLength = compassRadius - 6;
|
||||||
|
const arrowX = compassX + Math.sin(relativeBearing) * arrowLength;
|
||||||
|
const arrowY = compassY - Math.cos(relativeBearing) * arrowLength;
|
||||||
|
|
||||||
|
display.drawLine(compassX, compassY, arrowX, arrowY);
|
||||||
|
|
||||||
|
// Draw arrowhead
|
||||||
|
const headAngle = 0.4;
|
||||||
|
const headLength = 8;
|
||||||
|
const angle1 = relativeBearing + Math.PI - headAngle;
|
||||||
|
const angle2 = relativeBearing + Math.PI + headAngle;
|
||||||
|
display.drawLine(
|
||||||
|
arrowX,
|
||||||
|
arrowY,
|
||||||
|
arrowX + Math.sin(angle1) * headLength,
|
||||||
|
arrowY - Math.cos(angle1) * headLength,
|
||||||
|
);
|
||||||
|
display.drawLine(
|
||||||
|
arrowX,
|
||||||
|
arrowY,
|
||||||
|
arrowX + Math.sin(angle2) * headLength,
|
||||||
|
arrowY - Math.cos(angle2) * headLength,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw distance at bottom
|
||||||
|
if (compassInfo.distance !== undefined) {
|
||||||
|
let distStr: string;
|
||||||
|
if (compassInfo.distance >= 1000) {
|
||||||
|
distStr = `${(compassInfo.distance / 1000).toFixed(1)} km`;
|
||||||
|
} else {
|
||||||
|
distStr = `${Math.round(compassInfo.distance)} m`;
|
||||||
|
}
|
||||||
|
display.drawString(compassX, screenH - FONT_HEIGHT_SMALL, distStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a progress/loading screen
|
||||||
|
*/
|
||||||
|
export function drawProgressScreen(
|
||||||
|
display: OLEDDisplay,
|
||||||
|
title: string,
|
||||||
|
progress: number,
|
||||||
|
statusText?: string,
|
||||||
|
): void {
|
||||||
|
display.clear();
|
||||||
|
display.setFont(Font_6x8);
|
||||||
|
display.setColor(WHITE);
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
|
|
||||||
|
const screenW = display.getWidth();
|
||||||
|
const screenH = display.getHeight();
|
||||||
|
const centerX = screenW / 2;
|
||||||
|
const centerY = screenH / 2;
|
||||||
|
|
||||||
|
// Title
|
||||||
|
display.drawString(centerX, centerY - 20, title);
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
const barWidth = screenW - 20;
|
||||||
|
const barHeight = 10;
|
||||||
|
const barX = 10;
|
||||||
|
const barY = centerY - barHeight / 2;
|
||||||
|
|
||||||
|
display.drawProgressBar(barX, barY, barWidth, barHeight, progress);
|
||||||
|
|
||||||
|
// Progress percentage
|
||||||
|
display.drawString(centerX, barY + barHeight + 4, `${Math.round(progress)}%`);
|
||||||
|
|
||||||
|
// Status text
|
||||||
|
if (statusText) {
|
||||||
|
display.drawString(centerX, screenH - FONT_HEIGHT_SMALL - 2, statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
}
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
/**
|
||||||
|
* SimpleFonts.ts - Simple working bitmap fonts for OLED emulator
|
||||||
|
*
|
||||||
|
* These are basic monospace bitmap fonts that are easy to render correctly.
|
||||||
|
* Format: Each character is stored in column-major order, 1 byte per column.
|
||||||
|
* For fonts taller than 8px, multiple bytes per column are used.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { OLEDFont } from "./OLEDDisplay";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple 6x8 monospace font
|
||||||
|
* Each character is 6 columns wide, 8 rows tall
|
||||||
|
* 1 byte per column (8 vertical pixels)
|
||||||
|
* Includes ASCII 32-126
|
||||||
|
*/
|
||||||
|
export const Font_6x8: OLEDFont = {
|
||||||
|
height: 8,
|
||||||
|
firstChar: 32,
|
||||||
|
charCount: 95,
|
||||||
|
widths: new Array(95).fill(6), // Fixed width font
|
||||||
|
data: new Uint8Array([
|
||||||
|
// Space (32)
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// ! (33)
|
||||||
|
0x00, 0x00, 0x5f, 0x00, 0x00, 0x00,
|
||||||
|
// " (34)
|
||||||
|
0x00, 0x07, 0x00, 0x07, 0x00, 0x00,
|
||||||
|
// # (35)
|
||||||
|
0x14, 0x7f, 0x14, 0x7f, 0x14, 0x00,
|
||||||
|
// $ (36)
|
||||||
|
0x24, 0x2a, 0x7f, 0x2a, 0x12, 0x00,
|
||||||
|
// % (37)
|
||||||
|
0x23, 0x13, 0x08, 0x64, 0x62, 0x00,
|
||||||
|
// & (38)
|
||||||
|
0x36, 0x49, 0x55, 0x22, 0x50, 0x00,
|
||||||
|
// ' (39)
|
||||||
|
0x00, 0x05, 0x03, 0x00, 0x00, 0x00,
|
||||||
|
// ( (40)
|
||||||
|
0x00, 0x1c, 0x22, 0x41, 0x00, 0x00,
|
||||||
|
// ) (41)
|
||||||
|
0x00, 0x41, 0x22, 0x1c, 0x00, 0x00,
|
||||||
|
// * (42)
|
||||||
|
0x08, 0x2a, 0x1c, 0x2a, 0x08, 0x00,
|
||||||
|
// + (43)
|
||||||
|
0x08, 0x08, 0x3e, 0x08, 0x08, 0x00,
|
||||||
|
// , (44)
|
||||||
|
0x00, 0x50, 0x30, 0x00, 0x00, 0x00,
|
||||||
|
// - (45)
|
||||||
|
0x08, 0x08, 0x08, 0x08, 0x08, 0x00,
|
||||||
|
// . (46)
|
||||||
|
0x00, 0x60, 0x60, 0x00, 0x00, 0x00,
|
||||||
|
// / (47)
|
||||||
|
0x20, 0x10, 0x08, 0x04, 0x02, 0x00,
|
||||||
|
// 0 (48)
|
||||||
|
0x3e, 0x51, 0x49, 0x45, 0x3e, 0x00,
|
||||||
|
// 1 (49)
|
||||||
|
0x00, 0x42, 0x7f, 0x40, 0x00, 0x00,
|
||||||
|
// 2 (50)
|
||||||
|
0x42, 0x61, 0x51, 0x49, 0x46, 0x00,
|
||||||
|
// 3 (51)
|
||||||
|
0x21, 0x41, 0x45, 0x4b, 0x31, 0x00,
|
||||||
|
// 4 (52)
|
||||||
|
0x18, 0x14, 0x12, 0x7f, 0x10, 0x00,
|
||||||
|
// 5 (53)
|
||||||
|
0x27, 0x45, 0x45, 0x45, 0x39, 0x00,
|
||||||
|
// 6 (54)
|
||||||
|
0x3c, 0x4a, 0x49, 0x49, 0x30, 0x00,
|
||||||
|
// 7 (55)
|
||||||
|
0x01, 0x71, 0x09, 0x05, 0x03, 0x00,
|
||||||
|
// 8 (56)
|
||||||
|
0x36, 0x49, 0x49, 0x49, 0x36, 0x00,
|
||||||
|
// 9 (57)
|
||||||
|
0x06, 0x49, 0x49, 0x29, 0x1e, 0x00,
|
||||||
|
// : (58)
|
||||||
|
0x00, 0x36, 0x36, 0x00, 0x00, 0x00,
|
||||||
|
// ; (59)
|
||||||
|
0x00, 0x56, 0x36, 0x00, 0x00, 0x00,
|
||||||
|
// < (60)
|
||||||
|
0x00, 0x08, 0x14, 0x22, 0x41, 0x00,
|
||||||
|
// = (61)
|
||||||
|
0x14, 0x14, 0x14, 0x14, 0x14, 0x00,
|
||||||
|
// > (62)
|
||||||
|
0x41, 0x22, 0x14, 0x08, 0x00, 0x00,
|
||||||
|
// ? (63)
|
||||||
|
0x02, 0x01, 0x51, 0x09, 0x06, 0x00,
|
||||||
|
// @ (64)
|
||||||
|
0x32, 0x49, 0x79, 0x41, 0x3e, 0x00,
|
||||||
|
// A (65)
|
||||||
|
0x7e, 0x11, 0x11, 0x11, 0x7e, 0x00,
|
||||||
|
// B (66)
|
||||||
|
0x7f, 0x49, 0x49, 0x49, 0x36, 0x00,
|
||||||
|
// C (67)
|
||||||
|
0x3e, 0x41, 0x41, 0x41, 0x22, 0x00,
|
||||||
|
// D (68)
|
||||||
|
0x7f, 0x41, 0x41, 0x22, 0x1c, 0x00,
|
||||||
|
// E (69)
|
||||||
|
0x7f, 0x49, 0x49, 0x49, 0x41, 0x00,
|
||||||
|
// F (70)
|
||||||
|
0x7f, 0x09, 0x09, 0x01, 0x01, 0x00,
|
||||||
|
// G (71)
|
||||||
|
0x3e, 0x41, 0x41, 0x51, 0x32, 0x00,
|
||||||
|
// H (72)
|
||||||
|
0x7f, 0x08, 0x08, 0x08, 0x7f, 0x00,
|
||||||
|
// I (73)
|
||||||
|
0x00, 0x41, 0x7f, 0x41, 0x00, 0x00,
|
||||||
|
// J (74)
|
||||||
|
0x20, 0x40, 0x41, 0x3f, 0x01, 0x00,
|
||||||
|
// K (75)
|
||||||
|
0x7f, 0x08, 0x14, 0x22, 0x41, 0x00,
|
||||||
|
// L (76)
|
||||||
|
0x7f, 0x40, 0x40, 0x40, 0x40, 0x00,
|
||||||
|
// M (77)
|
||||||
|
0x7f, 0x02, 0x04, 0x02, 0x7f, 0x00,
|
||||||
|
// N (78)
|
||||||
|
0x7f, 0x04, 0x08, 0x10, 0x7f, 0x00,
|
||||||
|
// O (79)
|
||||||
|
0x3e, 0x41, 0x41, 0x41, 0x3e, 0x00,
|
||||||
|
// P (80)
|
||||||
|
0x7f, 0x09, 0x09, 0x09, 0x06, 0x00,
|
||||||
|
// Q (81)
|
||||||
|
0x3e, 0x41, 0x51, 0x21, 0x5e, 0x00,
|
||||||
|
// R (82)
|
||||||
|
0x7f, 0x09, 0x19, 0x29, 0x46, 0x00,
|
||||||
|
// S (83)
|
||||||
|
0x46, 0x49, 0x49, 0x49, 0x31, 0x00,
|
||||||
|
// T (84)
|
||||||
|
0x01, 0x01, 0x7f, 0x01, 0x01, 0x00,
|
||||||
|
// U (85)
|
||||||
|
0x3f, 0x40, 0x40, 0x40, 0x3f, 0x00,
|
||||||
|
// V (86)
|
||||||
|
0x1f, 0x20, 0x40, 0x20, 0x1f, 0x00,
|
||||||
|
// W (87)
|
||||||
|
0x7f, 0x20, 0x18, 0x20, 0x7f, 0x00,
|
||||||
|
// X (88)
|
||||||
|
0x63, 0x14, 0x08, 0x14, 0x63, 0x00,
|
||||||
|
// Y (89)
|
||||||
|
0x03, 0x04, 0x78, 0x04, 0x03, 0x00,
|
||||||
|
// Z (90)
|
||||||
|
0x61, 0x51, 0x49, 0x45, 0x43, 0x00,
|
||||||
|
// [ (91)
|
||||||
|
0x00, 0x00, 0x7f, 0x41, 0x41, 0x00,
|
||||||
|
// \ (92)
|
||||||
|
0x02, 0x04, 0x08, 0x10, 0x20, 0x00,
|
||||||
|
// ] (93)
|
||||||
|
0x41, 0x41, 0x7f, 0x00, 0x00, 0x00,
|
||||||
|
// ^ (94)
|
||||||
|
0x04, 0x02, 0x01, 0x02, 0x04, 0x00,
|
||||||
|
// _ (95)
|
||||||
|
0x40, 0x40, 0x40, 0x40, 0x40, 0x00,
|
||||||
|
// ` (96)
|
||||||
|
0x00, 0x01, 0x02, 0x04, 0x00, 0x00,
|
||||||
|
// a (97)
|
||||||
|
0x20, 0x54, 0x54, 0x54, 0x78, 0x00,
|
||||||
|
// b (98)
|
||||||
|
0x7f, 0x48, 0x44, 0x44, 0x38, 0x00,
|
||||||
|
// c (99)
|
||||||
|
0x38, 0x44, 0x44, 0x44, 0x20, 0x00,
|
||||||
|
// d (100)
|
||||||
|
0x38, 0x44, 0x44, 0x48, 0x7f, 0x00,
|
||||||
|
// e (101)
|
||||||
|
0x38, 0x54, 0x54, 0x54, 0x18, 0x00,
|
||||||
|
// f (102)
|
||||||
|
0x08, 0x7e, 0x09, 0x01, 0x02, 0x00,
|
||||||
|
// g (103)
|
||||||
|
0x08, 0x14, 0x54, 0x54, 0x3c, 0x00,
|
||||||
|
// h (104)
|
||||||
|
0x7f, 0x08, 0x04, 0x04, 0x78, 0x00,
|
||||||
|
// i (105)
|
||||||
|
0x00, 0x44, 0x7d, 0x40, 0x00, 0x00,
|
||||||
|
// j (106)
|
||||||
|
0x20, 0x40, 0x44, 0x3d, 0x00, 0x00,
|
||||||
|
// k (107)
|
||||||
|
0x00, 0x7f, 0x10, 0x28, 0x44, 0x00,
|
||||||
|
// l (108)
|
||||||
|
0x00, 0x41, 0x7f, 0x40, 0x00, 0x00,
|
||||||
|
// m (109)
|
||||||
|
0x7c, 0x04, 0x18, 0x04, 0x78, 0x00,
|
||||||
|
// n (110)
|
||||||
|
0x7c, 0x08, 0x04, 0x04, 0x78, 0x00,
|
||||||
|
// o (111)
|
||||||
|
0x38, 0x44, 0x44, 0x44, 0x38, 0x00,
|
||||||
|
// p (112)
|
||||||
|
0x7c, 0x14, 0x14, 0x14, 0x08, 0x00,
|
||||||
|
// q (113)
|
||||||
|
0x08, 0x14, 0x14, 0x18, 0x7c, 0x00,
|
||||||
|
// r (114)
|
||||||
|
0x7c, 0x08, 0x04, 0x04, 0x08, 0x00,
|
||||||
|
// s (115)
|
||||||
|
0x48, 0x54, 0x54, 0x54, 0x20, 0x00,
|
||||||
|
// t (116)
|
||||||
|
0x04, 0x3f, 0x44, 0x40, 0x20, 0x00,
|
||||||
|
// u (117)
|
||||||
|
0x3c, 0x40, 0x40, 0x20, 0x7c, 0x00,
|
||||||
|
// v (118)
|
||||||
|
0x1c, 0x20, 0x40, 0x20, 0x1c, 0x00,
|
||||||
|
// w (119)
|
||||||
|
0x3c, 0x40, 0x30, 0x40, 0x3c, 0x00,
|
||||||
|
// x (120)
|
||||||
|
0x44, 0x28, 0x10, 0x28, 0x44, 0x00,
|
||||||
|
// y (121)
|
||||||
|
0x0c, 0x50, 0x50, 0x50, 0x3c, 0x00,
|
||||||
|
// z (122)
|
||||||
|
0x44, 0x64, 0x54, 0x4c, 0x44, 0x00,
|
||||||
|
// { (123)
|
||||||
|
0x00, 0x08, 0x36, 0x41, 0x00, 0x00,
|
||||||
|
// | (124)
|
||||||
|
0x00, 0x00, 0x7f, 0x00, 0x00, 0x00,
|
||||||
|
// } (125)
|
||||||
|
0x00, 0x41, 0x36, 0x08, 0x00, 0x00,
|
||||||
|
// ~ (126)
|
||||||
|
0x08, 0x08, 0x2a, 0x1c, 0x08, 0x00,
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple 5x7 monospace font (more compact)
|
||||||
|
* Each character is 5 columns wide, 7 rows tall
|
||||||
|
* 1 byte per column
|
||||||
|
*/
|
||||||
|
export const Font_5x7: OLEDFont = {
|
||||||
|
height: 7,
|
||||||
|
firstChar: 32,
|
||||||
|
charCount: 95,
|
||||||
|
widths: new Array(95).fill(5),
|
||||||
|
data: new Uint8Array([
|
||||||
|
// Space (32)
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// ! (33)
|
||||||
|
0x00, 0x00, 0x2f, 0x00, 0x00,
|
||||||
|
// " (34)
|
||||||
|
0x00, 0x07, 0x00, 0x07, 0x00,
|
||||||
|
// # (35)
|
||||||
|
0x14, 0x3e, 0x14, 0x3e, 0x14,
|
||||||
|
// $ (36)
|
||||||
|
0x24, 0x2a, 0x7f, 0x2a, 0x12,
|
||||||
|
// % (37)
|
||||||
|
0x23, 0x13, 0x08, 0x64, 0x62,
|
||||||
|
// & (38)
|
||||||
|
0x36, 0x49, 0x55, 0x22, 0x50,
|
||||||
|
// ' (39)
|
||||||
|
0x00, 0x05, 0x03, 0x00, 0x00,
|
||||||
|
// ( (40)
|
||||||
|
0x00, 0x1c, 0x22, 0x41, 0x00,
|
||||||
|
// ) (41)
|
||||||
|
0x00, 0x41, 0x22, 0x1c, 0x00,
|
||||||
|
// * (42)
|
||||||
|
0x14, 0x08, 0x3e, 0x08, 0x14,
|
||||||
|
// + (43)
|
||||||
|
0x08, 0x08, 0x3e, 0x08, 0x08,
|
||||||
|
// , (44)
|
||||||
|
0x00, 0x50, 0x30, 0x00, 0x00,
|
||||||
|
// - (45)
|
||||||
|
0x08, 0x08, 0x08, 0x08, 0x08,
|
||||||
|
// . (46)
|
||||||
|
0x00, 0x30, 0x30, 0x00, 0x00,
|
||||||
|
// / (47)
|
||||||
|
0x20, 0x10, 0x08, 0x04, 0x02,
|
||||||
|
// 0 (48)
|
||||||
|
0x3e, 0x51, 0x49, 0x45, 0x3e,
|
||||||
|
// 1 (49)
|
||||||
|
0x00, 0x42, 0x7f, 0x40, 0x00,
|
||||||
|
// 2 (50)
|
||||||
|
0x42, 0x61, 0x51, 0x49, 0x46,
|
||||||
|
// 3 (51)
|
||||||
|
0x21, 0x41, 0x45, 0x4b, 0x31,
|
||||||
|
// 4 (52)
|
||||||
|
0x18, 0x14, 0x12, 0x7f, 0x10,
|
||||||
|
// 5 (53)
|
||||||
|
0x27, 0x45, 0x45, 0x45, 0x39,
|
||||||
|
// 6 (54)
|
||||||
|
0x3c, 0x4a, 0x49, 0x49, 0x30,
|
||||||
|
// 7 (55)
|
||||||
|
0x01, 0x71, 0x09, 0x05, 0x03,
|
||||||
|
// 8 (56)
|
||||||
|
0x36, 0x49, 0x49, 0x49, 0x36,
|
||||||
|
// 9 (57)
|
||||||
|
0x06, 0x49, 0x49, 0x29, 0x1e,
|
||||||
|
// : (58)
|
||||||
|
0x00, 0x36, 0x36, 0x00, 0x00,
|
||||||
|
// ; (59)
|
||||||
|
0x00, 0x56, 0x36, 0x00, 0x00,
|
||||||
|
// < (60)
|
||||||
|
0x08, 0x14, 0x22, 0x41, 0x00,
|
||||||
|
// = (61)
|
||||||
|
0x14, 0x14, 0x14, 0x14, 0x14,
|
||||||
|
// > (62)
|
||||||
|
0x00, 0x41, 0x22, 0x14, 0x08,
|
||||||
|
// ? (63)
|
||||||
|
0x02, 0x01, 0x51, 0x09, 0x06,
|
||||||
|
// @ (64)
|
||||||
|
0x32, 0x49, 0x79, 0x41, 0x3e,
|
||||||
|
// A (65)
|
||||||
|
0x7e, 0x11, 0x11, 0x11, 0x7e,
|
||||||
|
// B (66)
|
||||||
|
0x7f, 0x49, 0x49, 0x49, 0x36,
|
||||||
|
// C (67)
|
||||||
|
0x3e, 0x41, 0x41, 0x41, 0x22,
|
||||||
|
// D (68)
|
||||||
|
0x7f, 0x41, 0x41, 0x22, 0x1c,
|
||||||
|
// E (69)
|
||||||
|
0x7f, 0x49, 0x49, 0x49, 0x41,
|
||||||
|
// F (70)
|
||||||
|
0x7f, 0x09, 0x09, 0x09, 0x01,
|
||||||
|
// G (71)
|
||||||
|
0x3e, 0x41, 0x49, 0x49, 0x7a,
|
||||||
|
// H (72)
|
||||||
|
0x7f, 0x08, 0x08, 0x08, 0x7f,
|
||||||
|
// I (73)
|
||||||
|
0x00, 0x41, 0x7f, 0x41, 0x00,
|
||||||
|
// J (74)
|
||||||
|
0x20, 0x40, 0x41, 0x3f, 0x01,
|
||||||
|
// K (75)
|
||||||
|
0x7f, 0x08, 0x14, 0x22, 0x41,
|
||||||
|
// L (76)
|
||||||
|
0x7f, 0x40, 0x40, 0x40, 0x40,
|
||||||
|
// M (77)
|
||||||
|
0x7f, 0x02, 0x0c, 0x02, 0x7f,
|
||||||
|
// N (78)
|
||||||
|
0x7f, 0x04, 0x08, 0x10, 0x7f,
|
||||||
|
// O (79)
|
||||||
|
0x3e, 0x41, 0x41, 0x41, 0x3e,
|
||||||
|
// P (80)
|
||||||
|
0x7f, 0x09, 0x09, 0x09, 0x06,
|
||||||
|
// Q (81)
|
||||||
|
0x3e, 0x41, 0x51, 0x21, 0x5e,
|
||||||
|
// R (82)
|
||||||
|
0x7f, 0x09, 0x19, 0x29, 0x46,
|
||||||
|
// S (83)
|
||||||
|
0x46, 0x49, 0x49, 0x49, 0x31,
|
||||||
|
// T (84)
|
||||||
|
0x01, 0x01, 0x7f, 0x01, 0x01,
|
||||||
|
// U (85)
|
||||||
|
0x3f, 0x40, 0x40, 0x40, 0x3f,
|
||||||
|
// V (86)
|
||||||
|
0x1f, 0x20, 0x40, 0x20, 0x1f,
|
||||||
|
// W (87)
|
||||||
|
0x3f, 0x40, 0x38, 0x40, 0x3f,
|
||||||
|
// X (88)
|
||||||
|
0x63, 0x14, 0x08, 0x14, 0x63,
|
||||||
|
// Y (89)
|
||||||
|
0x07, 0x08, 0x70, 0x08, 0x07,
|
||||||
|
// Z (90)
|
||||||
|
0x61, 0x51, 0x49, 0x45, 0x43,
|
||||||
|
// [ (91)
|
||||||
|
0x00, 0x7f, 0x41, 0x41, 0x00,
|
||||||
|
// \ (92)
|
||||||
|
0x02, 0x04, 0x08, 0x10, 0x20,
|
||||||
|
// ] (93)
|
||||||
|
0x00, 0x41, 0x41, 0x7f, 0x00,
|
||||||
|
// ^ (94)
|
||||||
|
0x04, 0x02, 0x01, 0x02, 0x04,
|
||||||
|
// _ (95)
|
||||||
|
0x40, 0x40, 0x40, 0x40, 0x40,
|
||||||
|
// ` (96)
|
||||||
|
0x00, 0x01, 0x02, 0x04, 0x00,
|
||||||
|
// a (97)
|
||||||
|
0x20, 0x54, 0x54, 0x54, 0x78,
|
||||||
|
// b (98)
|
||||||
|
0x7f, 0x48, 0x44, 0x44, 0x38,
|
||||||
|
// c (99)
|
||||||
|
0x38, 0x44, 0x44, 0x44, 0x20,
|
||||||
|
// d (100)
|
||||||
|
0x38, 0x44, 0x44, 0x48, 0x7f,
|
||||||
|
// e (101)
|
||||||
|
0x38, 0x54, 0x54, 0x54, 0x18,
|
||||||
|
// f (102)
|
||||||
|
0x08, 0x7e, 0x09, 0x01, 0x02,
|
||||||
|
// g (103)
|
||||||
|
0x0c, 0x52, 0x52, 0x52, 0x3e,
|
||||||
|
// h (104)
|
||||||
|
0x7f, 0x08, 0x04, 0x04, 0x78,
|
||||||
|
// i (105)
|
||||||
|
0x00, 0x44, 0x7d, 0x40, 0x00,
|
||||||
|
// j (106)
|
||||||
|
0x20, 0x40, 0x44, 0x3d, 0x00,
|
||||||
|
// k (107)
|
||||||
|
0x7f, 0x10, 0x28, 0x44, 0x00,
|
||||||
|
// l (108)
|
||||||
|
0x00, 0x41, 0x7f, 0x40, 0x00,
|
||||||
|
// m (109)
|
||||||
|
0x7c, 0x04, 0x18, 0x04, 0x78,
|
||||||
|
// n (110)
|
||||||
|
0x7c, 0x08, 0x04, 0x04, 0x78,
|
||||||
|
// o (111)
|
||||||
|
0x38, 0x44, 0x44, 0x44, 0x38,
|
||||||
|
// p (112)
|
||||||
|
0x7c, 0x14, 0x14, 0x14, 0x08,
|
||||||
|
// q (113)
|
||||||
|
0x08, 0x14, 0x14, 0x18, 0x7c,
|
||||||
|
// r (114)
|
||||||
|
0x7c, 0x08, 0x04, 0x04, 0x08,
|
||||||
|
// s (115)
|
||||||
|
0x48, 0x54, 0x54, 0x54, 0x20,
|
||||||
|
// t (116)
|
||||||
|
0x04, 0x3f, 0x44, 0x40, 0x20,
|
||||||
|
// u (117)
|
||||||
|
0x3c, 0x40, 0x40, 0x20, 0x7c,
|
||||||
|
// v (118)
|
||||||
|
0x1c, 0x20, 0x40, 0x20, 0x1c,
|
||||||
|
// w (119)
|
||||||
|
0x3c, 0x40, 0x30, 0x40, 0x3c,
|
||||||
|
// x (120)
|
||||||
|
0x44, 0x28, 0x10, 0x28, 0x44,
|
||||||
|
// y (121)
|
||||||
|
0x0c, 0x50, 0x50, 0x50, 0x3c,
|
||||||
|
// z (122)
|
||||||
|
0x44, 0x64, 0x54, 0x4c, 0x44,
|
||||||
|
// { (123)
|
||||||
|
0x00, 0x08, 0x36, 0x41, 0x00,
|
||||||
|
// | (124)
|
||||||
|
0x00, 0x00, 0x7f, 0x00, 0x00,
|
||||||
|
// } (125)
|
||||||
|
0x00, 0x41, 0x36, 0x08, 0x00,
|
||||||
|
// ~ (126)
|
||||||
|
0x10, 0x08, 0x08, 0x10, 0x08,
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Alias for compatibility
|
||||||
|
export const SimpleFont = Font_6x8;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Meshtastic OLED Emulator
|
||||||
|
*
|
||||||
|
* A Vue.js component and TypeScript library for emulating SSD1306/SH1106 OLED displays.
|
||||||
|
* Provides the same OLEDDisplay API as the Meshtastic firmware, enabling
|
||||||
|
* screen development and testing in the browser.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Core display class
|
||||||
|
export {
|
||||||
|
OLEDDisplay,
|
||||||
|
type OLEDDISPLAY_GEOMETRY,
|
||||||
|
type OLEDDISPLAY_COLOR,
|
||||||
|
type OLEDDISPLAY_TEXT_ALIGNMENT,
|
||||||
|
type OLEDFont,
|
||||||
|
} from "./OLEDDisplay";
|
||||||
|
export {
|
||||||
|
WHITE,
|
||||||
|
BLACK,
|
||||||
|
INVERSE,
|
||||||
|
TEXT_ALIGN_LEFT,
|
||||||
|
TEXT_ALIGN_CENTER,
|
||||||
|
TEXT_ALIGN_RIGHT,
|
||||||
|
} from "./OLEDDisplay";
|
||||||
|
|
||||||
|
// Fonts
|
||||||
|
export {
|
||||||
|
ArialMT_Plain_10,
|
||||||
|
ArialMT_Plain_16,
|
||||||
|
FONT_HEIGHT_SMALL,
|
||||||
|
FONT_HEIGHT_MEDIUM,
|
||||||
|
FONT_HEIGHT_LARGE,
|
||||||
|
createFontFromBytes,
|
||||||
|
} from "./OLEDFonts";
|
||||||
|
|
||||||
|
// Simple working fonts (recommended)
|
||||||
|
export { Font_6x8, Font_5x7, SimpleFont } from "./SimpleFonts";
|
||||||
|
|
||||||
|
// Images and icons
|
||||||
|
export * as OLEDImages from "./OLEDImages";
|
||||||
|
|
||||||
|
// Screen renderers (firmware UI ports)
|
||||||
|
export * as ScreenRenderers from "./ScreenRenderers";
|
||||||
|
|
||||||
|
// Vue component
|
||||||
|
export { default as OLEDEmulator } from "./OLEDEmulator.vue";
|
||||||
|
export { DISPLAY_PRESETS, type DisplayPreset } from "./DisplayPresets";
|
||||||
|
|
||||||
|
// Demo component
|
||||||
|
export { default as OLEDEmulatorDemo } from "./Demo.vue";
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import Demo from "./Demo.vue";
|
||||||
|
|
||||||
|
createApp(Demo).mount("#app");
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
/* Vue */
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import dts from "vite-plugin-dts";
|
||||||
|
import { resolve } from "path";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
dts({
|
||||||
|
insertTypesEntry: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, "src/index.ts"),
|
||||||
|
name: "MeshtasticOLEDEmulator",
|
||||||
|
formats: ["es", "cjs"],
|
||||||
|
fileName: (format) => `index.${format === "es" ? "mjs" : "js"}`,
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ["vue"],
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
vue: "Vue",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cssCodeSplit: false,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": resolve(__dirname, "src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
+14
-6
@@ -50,12 +50,15 @@ 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
|
||||||
|
-DMESHTASTIC_EXCLUDE_STATUS=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 +68,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 +122,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/69739b84f87a91568d3c421498bc89977937a141.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 +146,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.9
|
||||||
# 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
|
||||||
@@ -209,9 +212,12 @@ lib_deps =
|
|||||||
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
|
# 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
|
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
|
||||||
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
|
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
|
||||||
sensirion/Sensirion Core@0.7.2
|
sensirion/Sensirion Core@0.7.3
|
||||||
# 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 SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
|
||||||
|
sensirion/Sensirion I2C SFA3x@1.0.0
|
||||||
|
|
||||||
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
|
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
|
||||||
[environmental_extra_no_bsec]
|
[environmental_extra_no_bsec]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
@@ -236,6 +242,8 @@ lib_deps =
|
|||||||
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
|
# 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
|
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
|
||||||
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
|
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
|
||||||
sensirion/Sensirion Core@0.7.2
|
sensirion/Sensirion Core@0.7.3
|
||||||
# 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 SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
|
||||||
|
sensirion/Sensirion I2C SFA3x@1.0.0
|
||||||
|
|||||||
+1
-1
Submodule protobufs updated: bc63a57f9e...44298d374f
@@ -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
|
||||||
+66
-59
@@ -1,19 +1,21 @@
|
|||||||
|
#ifndef AMBIENTLIGHTINGTHREAD_H
|
||||||
|
#define AMBIENTLIGHTINGTHREAD_H
|
||||||
|
|
||||||
#include "Observer.h"
|
#include "Observer.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "detect/ScanI2C.h"
|
||||||
|
#include "sleep.h"
|
||||||
|
|
||||||
#ifdef HAS_NCP5623
|
#ifdef HAS_NCP5623
|
||||||
#include <graphics/RAKled.h>
|
#include <NCP5623.h>
|
||||||
NCP5623 rgb;
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAS_LP5562
|
#ifdef HAS_LP5562
|
||||||
#include <graphics/NomadStarLED.h>
|
#include <graphics/NomadStarLED.h>
|
||||||
LP5562 rgbw;
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAS_NEOPIXEL
|
#ifdef HAS_NEOPIXEL
|
||||||
#include <graphics/NeoPixel.h>
|
#include <Adafruit_NeoPixel.h>
|
||||||
Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE);
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef UNPHONE
|
#ifdef UNPHONE
|
||||||
@@ -21,10 +23,24 @@ Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE);
|
|||||||
extern unPhone unphone;
|
extern unPhone unphone;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace concurrency
|
|
||||||
{
|
|
||||||
class AmbientLightingThread : public concurrency::OSThread
|
class AmbientLightingThread : public concurrency::OSThread
|
||||||
{
|
{
|
||||||
|
friend class StatusLEDModule; // Let the LEDStatusModule trigger the ambient lighting for notifications and battery status.
|
||||||
|
friend class ExternalNotificationModule; // Let the ExternalNotificationModule trigger the ambient lighting for notifications.
|
||||||
|
|
||||||
|
private:
|
||||||
|
#ifdef HAS_NCP5623
|
||||||
|
NCP5623 rgb;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAS_LP5562
|
||||||
|
LP5562 rgbw;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAS_NEOPIXEL
|
||||||
|
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE);
|
||||||
|
#endif
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting")
|
explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting")
|
||||||
{
|
{
|
||||||
@@ -36,14 +52,15 @@ class AmbientLightingThread : public concurrency::OSThread
|
|||||||
moduleConfig.ambient_lighting.led_state = true;
|
moduleConfig.ambient_lighting.led_state = true;
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
// Uncomment to test module
|
#if AMBIENT_LIGHTING_TEST
|
||||||
// moduleConfig.ambient_lighting.led_state = true;
|
// define to enable test
|
||||||
// moduleConfig.ambient_lighting.current = 10;
|
moduleConfig.ambient_lighting.led_state = true;
|
||||||
|
moduleConfig.ambient_lighting.current = 10;
|
||||||
// Default to a color based on our node number
|
// Default to a color based on our node number
|
||||||
// moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
|
moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
|
||||||
// moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
|
moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
|
||||||
// moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
|
moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
|
||||||
|
#endif
|
||||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||||
_type = type;
|
_type = type;
|
||||||
if (_type == ScanI2C::DeviceType::NONE) {
|
if (_type == ScanI2C::DeviceType::NONE) {
|
||||||
@@ -53,11 +70,6 @@ class AmbientLightingThread : public concurrency::OSThread
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef HAS_RGB_LED
|
#ifdef HAS_RGB_LED
|
||||||
if (!moduleConfig.ambient_lighting.led_state) {
|
|
||||||
LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF");
|
|
||||||
disable();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
LOG_DEBUG("AmbientLighting init");
|
LOG_DEBUG("AmbientLighting init");
|
||||||
#ifdef HAS_NCP5623
|
#ifdef HAS_NCP5623
|
||||||
if (_type == ScanI2C::NCP5623) {
|
if (_type == ScanI2C::NCP5623) {
|
||||||
@@ -77,7 +89,13 @@ class AmbientLightingThread : public concurrency::OSThread
|
|||||||
pixels.clear(); // Set all pixel colors to 'off'
|
pixels.clear(); // Set all pixel colors to 'off'
|
||||||
pixels.setBrightness(moduleConfig.ambient_lighting.current);
|
pixels.setBrightness(moduleConfig.ambient_lighting.current);
|
||||||
#endif
|
#endif
|
||||||
setLighting();
|
if (!moduleConfig.ambient_lighting.led_state) {
|
||||||
|
LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF");
|
||||||
|
disable();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
||||||
|
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||||
#endif
|
#endif
|
||||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||||
}
|
}
|
||||||
@@ -91,7 +109,8 @@ class AmbientLightingThread : public concurrency::OSThread
|
|||||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||||
if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) {
|
if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) {
|
||||||
#endif
|
#endif
|
||||||
setLighting();
|
setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
||||||
|
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||||
return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification
|
return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification
|
||||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||||
}
|
}
|
||||||
@@ -148,65 +167,53 @@ class AmbientLightingThread : public concurrency::OSThread
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLighting()
|
protected:
|
||||||
|
void setLighting(float current, uint8_t red, uint8_t green, uint8_t blue)
|
||||||
{
|
{
|
||||||
#ifdef HAS_NCP5623
|
#ifdef HAS_NCP5623
|
||||||
rgb.setCurrent(moduleConfig.ambient_lighting.current);
|
rgb.setCurrent(current);
|
||||||
rgb.setRed(moduleConfig.ambient_lighting.red);
|
rgb.setRed(red);
|
||||||
rgb.setGreen(moduleConfig.ambient_lighting.green);
|
rgb.setGreen(green);
|
||||||
rgb.setBlue(moduleConfig.ambient_lighting.blue);
|
rgb.setBlue(blue);
|
||||||
LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d",
|
LOG_DEBUG("Init NCP5623 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue);
|
||||||
moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
|
||||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef HAS_LP5562
|
#ifdef HAS_LP5562
|
||||||
rgbw.setCurrent(moduleConfig.ambient_lighting.current);
|
rgbw.setCurrent(current);
|
||||||
rgbw.setRed(moduleConfig.ambient_lighting.red);
|
rgbw.setRed(red);
|
||||||
rgbw.setGreen(moduleConfig.ambient_lighting.green);
|
rgbw.setGreen(green);
|
||||||
rgbw.setBlue(moduleConfig.ambient_lighting.blue);
|
rgbw.setBlue(blue);
|
||||||
LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current,
|
LOG_DEBUG("Init LP5562 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue);
|
||||||
moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef HAS_NEOPIXEL
|
#ifdef HAS_NEOPIXEL
|
||||||
pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green,
|
pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT);
|
||||||
moduleConfig.ambient_lighting.blue),
|
|
||||||
0, NEOPIXEL_COUNT);
|
|
||||||
|
|
||||||
// RadioMaster Bandit has addressable LED at the two buttons
|
// RadioMaster Bandit has addressable LED at the two buttons
|
||||||
// this allow us to set different lighting for them in variant.h file.
|
// this allow us to set different lighting for them in variant.h file.
|
||||||
#ifdef RADIOMASTER_900_BANDIT
|
|
||||||
#if defined(BUTTON1_COLOR) && defined(BUTTON1_COLOR_INDEX)
|
#if defined(BUTTON1_COLOR) && defined(BUTTON1_COLOR_INDEX)
|
||||||
pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1);
|
pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1);
|
||||||
#endif
|
#endif
|
||||||
#if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX)
|
#if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX)
|
||||||
pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1);
|
pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1);
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
pixels.show();
|
pixels.show();
|
||||||
// LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d",
|
// LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%f, red=%d, green=%d, blue=%d",
|
||||||
// moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
// current, red, green, blue);
|
||||||
// moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef RGBLED_CA
|
#ifdef RGBLED_CA
|
||||||
analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red);
|
analogWrite(RGBLED_RED, 255 - red);
|
||||||
analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green);
|
analogWrite(RGBLED_GREEN, 255 - green);
|
||||||
analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue);
|
analogWrite(RGBLED_BLUE, 255 - blue);
|
||||||
LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
|
LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", red, green, blue);
|
||||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
|
||||||
#elif defined(RGBLED_RED)
|
#elif defined(RGBLED_RED)
|
||||||
analogWrite(RGBLED_RED, moduleConfig.ambient_lighting.red);
|
analogWrite(RGBLED_RED, red);
|
||||||
analogWrite(RGBLED_GREEN, moduleConfig.ambient_lighting.green);
|
analogWrite(RGBLED_GREEN, green);
|
||||||
analogWrite(RGBLED_BLUE, moduleConfig.ambient_lighting.blue);
|
analogWrite(RGBLED_BLUE, blue);
|
||||||
LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
|
LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", red, green, blue);
|
||||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef UNPHONE
|
#ifdef UNPHONE
|
||||||
unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green,
|
unphone.rgb(red, green, blue);
|
||||||
moduleConfig.ambient_lighting.blue);
|
LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", red, green, blue);
|
||||||
LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
|
|
||||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
#endif // AMBIENTLIGHTINGTHREAD_H
|
||||||
} // namespace concurrency
|
|
||||||
+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;
|
|
||||||
+96
-8
@@ -459,6 +459,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
}
|
}
|
||||||
// if it's not HIGH - check the battery
|
// if it's not HIGH - check the battery
|
||||||
#endif
|
#endif
|
||||||
|
// If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it.
|
||||||
|
return false;
|
||||||
|
|
||||||
// technically speaking this should work for all(?) NRF52 boards
|
// technically speaking this should work for all(?) NRF52 boards
|
||||||
// but needs testing across multiple devices. NRF52 USB would not even work if
|
// but needs testing across multiple devices. NRF52 USB would not even work if
|
||||||
@@ -690,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;
|
||||||
@@ -816,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)
|
||||||
@@ -1316,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;
|
||||||
@@ -1364,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1383,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;
|
||||||
}
|
}
|
||||||
|
|||||||
+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);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-6
@@ -217,6 +217,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define SHTC3_ADDR 0x70
|
#define SHTC3_ADDR 0x70
|
||||||
#define LPS22HB_ADDR 0x5C
|
#define LPS22HB_ADDR 0x5C
|
||||||
#define LPS22HB_ADDR_ALT 0x5D
|
#define LPS22HB_ADDR_ALT 0x5D
|
||||||
|
#define SFA30_ADDR 0x5D
|
||||||
#define SHT31_4x_ADDR 0x44
|
#define SHT31_4x_ADDR 0x44
|
||||||
#define SHT31_4x_ADDR_ALT 0x45
|
#define SHT31_4x_ADDR_ALT 0x45
|
||||||
#define PMSA003I_ADDR 0x12
|
#define PMSA003I_ADDR 0x12
|
||||||
@@ -233,6 +234,7 @@ 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 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
|
||||||
@@ -241,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
|
||||||
@@ -390,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
|
||||||
@@ -428,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)
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ 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, SFA30};
|
||||||
return firstOfOrNONE(2, types);
|
return firstOfOrNONE(4, types);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanI2C::FoundDevice ScanI2C::firstRGBLED() const
|
ScanI2C::FoundDevice ScanI2C::firstRGBLED() const
|
||||||
|
|||||||
@@ -88,7 +88,10 @@ class ScanI2C
|
|||||||
BH1750,
|
BH1750,
|
||||||
DA217,
|
DA217,
|
||||||
CHSC6X,
|
CHSC6X,
|
||||||
CST226SE
|
CST226SE,
|
||||||
|
SEN5X,
|
||||||
|
SFA30,
|
||||||
|
CW2015
|
||||||
} DeviceType;
|
} DeviceType;
|
||||||
|
|
||||||
// typedef uint8_t DeviceAddress;
|
// typedef uint8_t DeviceAddress;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#include "ScanI2CTwoWire.h"
|
#include "ScanI2CTwoWire.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
#include "detect/ScanI2C.h"
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_I2C
|
#if !MESHTASTIC_EXCLUDE_I2C
|
||||||
|
|
||||||
@@ -8,6 +10,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 +117,45 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
|
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
|
||||||
case ADDR: \
|
case ADDR: \
|
||||||
logFoundDevice(__VA_ARGS__); \
|
logFoundDevice(__VA_ARGS__); \
|
||||||
@@ -416,6 +458,13 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case LPS22HB_ADDR_ALT:
|
case LPS22HB_ADDR_ALT:
|
||||||
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD060), 48); // get device marking
|
||||||
|
if (registerValue != 0) {
|
||||||
|
type = SFA30;
|
||||||
|
logFoundDevice("SFA30", (uint8_t)addr.address);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// TODO - What happens with these two?
|
||||||
SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address)
|
SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address)
|
||||||
SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address)
|
SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address)
|
||||||
|
|
||||||
@@ -541,7 +590,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 +627,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 +640,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
|
||||||
|
|||||||
+6
-6
@@ -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
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
#ifdef HAS_NEOPIXEL
|
|
||||||
#include <Adafruit_NeoPixel.h>
|
|
||||||
extern Adafruit_NeoPixel pixels;
|
|
||||||
#endif
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#ifdef HAS_NCP5623
|
|
||||||
#include <NCP5623.h>
|
|
||||||
extern NCP5623 rgb;
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -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);
|
||||||
@@ -428,39 +427,33 @@ const int *getTextPositions(OLEDDisplay *display)
|
|||||||
// *************************
|
// *************************
|
||||||
void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
bool drawConnectionState = false;
|
if (!isAPIConnected(service->api_state))
|
||||||
if (service->api_state == service->STATE_BLE || service->api_state == service->STATE_WIFI ||
|
return;
|
||||||
service->api_state == service->STATE_SERIAL || service->api_state == service->STATE_PACKET ||
|
|
||||||
service->api_state == service->STATE_HTTP || service->api_state == service->STATE_ETH) {
|
|
||||||
drawConnectionState = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (drawConnectionState) {
|
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
|
||||||
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
|
display->setColor(BLACK);
|
||||||
display->setColor(BLACK);
|
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
|
||||||
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
|
(connection_icon_height * scale) + (2 * scale));
|
||||||
(connection_icon_height * scale) + (2 * scale));
|
display->setColor(WHITE);
|
||||||
display->setColor(WHITE);
|
if (currentResolution == ScreenResolution::High) {
|
||||||
if (currentResolution == ScreenResolution::High) {
|
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||||||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
int iconX = 0;
|
||||||
int iconX = 0;
|
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
||||||
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
|
||||||
|
|
||||||
for (int yy = 0; yy < connection_icon_height; ++yy) {
|
for (int yy = 0; yy < connection_icon_height; ++yy) {
|
||||||
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
|
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
|
||||||
for (int xx = 0; xx < connection_icon_width; ++xx) {
|
for (int xx = 0; xx < connection_icon_width; ++xx) {
|
||||||
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
||||||
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
|
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
|
||||||
if (byteVal & bitMask) {
|
if (byteVal & bitMask) {
|
||||||
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
|
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
|
||||||
display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height,
|
|
||||||
connection_icon);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height,
|
||||||
|
connection_icon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,4 +63,18 @@ bool isAllowedPunctuation(char c);
|
|||||||
|
|
||||||
std::string sanitizeString(const std::string &input);
|
std::string sanitizeString(const std::string &input);
|
||||||
|
|
||||||
|
static inline bool isAPIConnected(uint8_t state)
|
||||||
|
{
|
||||||
|
static constexpr bool connectedStates[] = {
|
||||||
|
/* STATE_NONE */ false,
|
||||||
|
/* STATE_BLE */ true,
|
||||||
|
/* STATE_WIFI */ true,
|
||||||
|
/* STATE_SERIAL */ true,
|
||||||
|
/* STATE_PACKET */ true,
|
||||||
|
/* STATE_HTTP */ true,
|
||||||
|
/* STATE_ETH */ true,
|
||||||
|
};
|
||||||
|
return state < sizeof(connectedStates) ? connectedStates[state] : false;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
|
|||||||
@@ -110,14 +110,14 @@ void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, ui
|
|||||||
uint32_t secs = (uptimeMillis % 60000) / 1000;
|
uint32_t secs = (uptimeMillis % 60000) / 1000;
|
||||||
|
|
||||||
if (days) {
|
if (days) {
|
||||||
snprintf(uptimeStr, maxLength, "%s: %ud %uh", prefix, days, hours);
|
snprintf(uptimeStr, maxLength, "%s%ud %uh", prefix, days, hours);
|
||||||
} else if (hours) {
|
} else if (hours) {
|
||||||
snprintf(uptimeStr, maxLength, "%s: %uh %um", prefix, hours, mins);
|
snprintf(uptimeStr, maxLength, "%s%uh %um", prefix, hours, mins);
|
||||||
} else if (!includeSecs) {
|
} else if (!includeSecs) {
|
||||||
snprintf(uptimeStr, maxLength, "%s: %um", prefix, mins);
|
snprintf(uptimeStr, maxLength, "%s%um", prefix, mins);
|
||||||
} else if (mins) {
|
} else if (mins) {
|
||||||
snprintf(uptimeStr, maxLength, "%s: %um %us", prefix, mins, secs);
|
snprintf(uptimeStr, maxLength, "%s%um %us", prefix, mins, secs);
|
||||||
} else {
|
} else {
|
||||||
snprintf(uptimeStr, maxLength, "%s: %us", prefix, secs);
|
snprintf(uptimeStr, maxLength, "%s%us", prefix, secs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -663,7 +663,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
|||||||
|
|
||||||
if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show uptime if the screen can show it
|
if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show uptime if the screen can show it
|
||||||
char uptimeStr[32] = "";
|
char uptimeStr[32] = "";
|
||||||
getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr));
|
getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr));
|
||||||
textWidth = display->getStringWidth(uptimeStr);
|
textWidth = display->getStringWidth(uptimeStr);
|
||||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||||
display->drawString(nameX, getTextPositions(display)[line++], uptimeStr);
|
display->drawString(nameX, getTextPositions(display)[line++], uptimeStr);
|
||||||
|
|||||||
+169
-132
@@ -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) {
|
||||||
@@ -300,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;
|
||||||
}
|
}
|
||||||
@@ -313,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},
|
||||||
@@ -333,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;
|
||||||
}
|
}
|
||||||
@@ -352,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 };
|
||||||
@@ -362,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;
|
||||||
@@ -390,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},
|
||||||
@@ -404,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;
|
||||||
}
|
}
|
||||||
@@ -456,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;
|
||||||
}
|
}
|
||||||
@@ -503,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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -572,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) {
|
||||||
@@ -589,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
|
||||||
@@ -649,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;
|
||||||
}
|
}
|
||||||
@@ -737,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;
|
||||||
}
|
}
|
||||||
@@ -901,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);
|
||||||
@@ -1083,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) {
|
||||||
@@ -1177,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) {
|
||||||
@@ -1238,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:
|
||||||
@@ -1255,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;
|
||||||
}
|
}
|
||||||
@@ -1303,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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1330,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);
|
||||||
@@ -1391,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;
|
||||||
}
|
}
|
||||||
@@ -1483,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;
|
||||||
}
|
}
|
||||||
@@ -1498,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");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1528,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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1550,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;
|
||||||
}
|
}
|
||||||
@@ -1595,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;
|
||||||
}
|
}
|
||||||
@@ -1660,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;
|
||||||
}
|
}
|
||||||
@@ -1715,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;
|
||||||
@@ -1744,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;
|
||||||
@@ -1832,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;
|
||||||
@@ -1915,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;
|
||||||
@@ -2043,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;
|
||||||
}
|
}
|
||||||
@@ -2138,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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2160,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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2221,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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2252,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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2301,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
|
||||||
@@ -2325,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;
|
||||||
@@ -2332,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;
|
||||||
@@ -2343,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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2380,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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2427,7 +2434,7 @@ void menuHandler::keyVerificationFinalPrompt()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void menuHandler::FrameToggles_menu()
|
void menuHandler::frameTogglesMenu()
|
||||||
{
|
{
|
||||||
enum optionsNumbers {
|
enum optionsNumbers {
|
||||||
Finish,
|
Finish,
|
||||||
@@ -2571,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 };
|
||||||
|
|
||||||
@@ -2592,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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2601,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 */
|
||||||
|
|||||||
@@ -527,8 +527,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
constexpr int BUBBLE_MIN_W = 24;
|
constexpr int BUBBLE_MIN_W = 24;
|
||||||
constexpr int BUBBLE_TEXT_INDENT = 2;
|
constexpr int BUBBLE_TEXT_INDENT = 2;
|
||||||
|
|
||||||
|
// 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
|
// Derived widths
|
||||||
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (BUBBLE_PAD_X * 2);
|
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
|
||||||
@@ -796,114 +800,105 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw bubbles
|
// Draw bubbles (only if enabled)
|
||||||
for (size_t bi = 0; bi < blocks.size(); ++bi) {
|
if (showBubbles) {
|
||||||
const auto &b = blocks[bi];
|
for (size_t bi = 0; bi < blocks.size(); ++bi) {
|
||||||
if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end)
|
const auto &b = blocks[bi];
|
||||||
continue;
|
if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end)
|
||||||
|
continue;
|
||||||
|
|
||||||
int visualTop = lineTop[b.start];
|
int visualTop = lineTop[b.start];
|
||||||
|
|
||||||
int topY;
|
int topY;
|
||||||
if (isHeader[b.start]) {
|
if (isHeader[b.start]) {
|
||||||
// Header start
|
// Header start
|
||||||
constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2
|
constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2
|
||||||
topY = visualTop - BUBBLE_PAD_TOP_HEADER;
|
topY = visualTop - BUBBLE_PAD_TOP_HEADER;
|
||||||
} else {
|
} else {
|
||||||
// Body start
|
// Body start
|
||||||
bool thisLineHasEmote = false;
|
bool thisLineHasEmote = false;
|
||||||
for (int e = 0; e < numEmotes; ++e) {
|
for (int e = 0; e < numEmotes; ++e) {
|
||||||
if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
|
if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
|
||||||
thisLineHasEmote = true;
|
thisLineHasEmote = true;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (thisLineHasEmote) {
|
||||||
|
constexpr int EMOTE_PADDING_ABOVE = 4;
|
||||||
|
visualTop -= EMOTE_PADDING_ABOVE;
|
||||||
|
}
|
||||||
|
topY = visualTop - BUBBLE_PAD_Y;
|
||||||
}
|
}
|
||||||
if (thisLineHasEmote) {
|
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
|
||||||
constexpr int EMOTE_PADDING_ABOVE = 4;
|
int bottomY = visualBottom + BUBBLE_PAD_Y;
|
||||||
visualTop -= EMOTE_PADDING_ABOVE;
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
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()) {
|
if (bottomY <= topY + 2)
|
||||||
int nextHeaderIndex = (int)blocks[bi + 1].start;
|
continue;
|
||||||
int nextTop = lineTop[nextHeaderIndex];
|
|
||||||
int maxBottom = nextTop - 1 - bubbleGapY;
|
|
||||||
if (bottomY > maxBottom)
|
|
||||||
bottomY = maxBottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bottomY <= topY + 2)
|
if (bottomY < contentTop || topY > contentBottom - 1)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (bottomY < contentTop || topY > contentBottom - 1)
|
int maxLineW = 0;
|
||||||
continue;
|
|
||||||
|
|
||||||
int maxLineW = 0;
|
for (size_t i = b.start; i <= b.end; ++i) {
|
||||||
|
int w = 0;
|
||||||
for (size_t i = b.start; i <= b.end; ++i) {
|
if (isHeader[i]) {
|
||||||
int w = 0;
|
w = display->getStringWidth(cachedLines[i].c_str());
|
||||||
if (isHeader[i]) {
|
if (b.mine)
|
||||||
w = display->getStringWidth(cachedLines[i].c_str());
|
w += 12; // room for ACK/NACK/relay mark
|
||||||
if (b.mine)
|
} else {
|
||||||
w += 12; // room for ACK/NACK/relay mark
|
w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
|
||||||
} else {
|
}
|
||||||
w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
|
if (w > maxLineW)
|
||||||
|
maxLineW = w;
|
||||||
}
|
}
|
||||||
if (w > maxLineW)
|
|
||||||
maxLineW = w;
|
|
||||||
}
|
|
||||||
|
|
||||||
int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (BUBBLE_PAD_X * 2));
|
|
||||||
int bubbleH = (bottomY - topY) + 1;
|
|
||||||
int bubbleX = 0;
|
|
||||||
if (b.mine) {
|
|
||||||
bubbleX = rightEdge - bubbleW;
|
|
||||||
} else {
|
|
||||||
bubbleX = x;
|
|
||||||
}
|
|
||||||
if (bubbleX < x)
|
|
||||||
bubbleX = x;
|
|
||||||
if (bubbleX + bubbleW > rightEdge)
|
|
||||||
bubbleW = std::max(1, rightEdge - bubbleX);
|
|
||||||
|
|
||||||
if (bubbleW > 1 && bubbleH > 1) {
|
|
||||||
int x1 = bubbleX + bubbleW - 1;
|
|
||||||
int y1 = topY + bubbleH - 1;
|
|
||||||
|
|
||||||
|
int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (textIndent * 2));
|
||||||
|
int bubbleH = (bottomY - topY) + 1;
|
||||||
|
int bubbleX = 0;
|
||||||
if (b.mine) {
|
if (b.mine) {
|
||||||
// Send Message (Right side)
|
bubbleX = rightEdge - bubbleW;
|
||||||
display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH);
|
|
||||||
// Top Right Corner
|
|
||||||
display->drawRect(x1, topY, 2, 1);
|
|
||||||
display->drawRect(x1, topY, 1, 2);
|
|
||||||
// Bottom Right Corner
|
|
||||||
display->drawRect(x1 - 1, bottomY - 2, 2, 1);
|
|
||||||
display->drawRect(x1, bottomY - 3, 1, 2);
|
|
||||||
// Knock the corners off to make a bubble
|
|
||||||
display->setColor(BLACK);
|
|
||||||
display->drawRect(x1 - bubbleW, topY - 1, 1, 1);
|
|
||||||
display->drawRect(x1 - bubbleW, bottomY - 1, 1, 1);
|
|
||||||
display->setColor(WHITE);
|
|
||||||
} else {
|
} else {
|
||||||
// Received Message (Left Side)
|
bubbleX = x;
|
||||||
display->drawRect(bubbleX, topY, bubbleW + 1, bubbleH);
|
}
|
||||||
// Top Left Corner
|
if (bubbleX < x)
|
||||||
display->drawRect(bubbleX + 1, topY + 1, 2, 1);
|
bubbleX = x;
|
||||||
display->drawRect(bubbleX + 1, topY + 1, 1, 2);
|
if (bubbleX + bubbleW > rightEdge)
|
||||||
// Bottom Left Corner
|
bubbleW = std::max(1, rightEdge - bubbleX);
|
||||||
display->drawRect(bubbleX + 1, bottomY - 1, 2, 1);
|
|
||||||
display->drawRect(bubbleX + 1, bottomY - 2, 1, 2);
|
// Draw rounded rectangle bubble
|
||||||
// Knock the corners off to make a bubble
|
if (bubbleW > BUBBLE_RADIUS * 2 && bubbleH > BUBBLE_RADIUS * 2) {
|
||||||
display->setColor(BLACK);
|
const int r = BUBBLE_RADIUS;
|
||||||
display->drawRect(bubbleX + bubbleW, topY, 1, 1);
|
const int bx = bubbleX;
|
||||||
display->drawRect(bubbleX + bubbleW, bottomY, 1, 1);
|
const int by = topY;
|
||||||
display->setColor(WHITE);
|
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;
|
int lineY = yOffset;
|
||||||
@@ -916,11 +911,11 @@ 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 - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - BUBBLE_TEXT_INDENT;
|
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 + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT;
|
headerX = x + textIndent;
|
||||||
}
|
}
|
||||||
display->drawString(headerX, lineY, cachedLines[i].c_str());
|
display->drawString(headerX, lineY, cachedLines[i].c_str());
|
||||||
|
|
||||||
@@ -960,14 +955,13 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
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 - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - BUBBLE_TEXT_INDENT;
|
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 + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT, lineY, cachedLines[i], emotes,
|
drawStringWithEmotes(display, x + textIndent, lineY, cachedLines[i], emotes, numEmotes);
|
||||||
numEmotes);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#if HAS_SCREEN
|
#if HAS_SCREEN
|
||||||
#include "CompassRenderer.h"
|
#include "CompassRenderer.h"
|
||||||
#include "GPSStatus.h"
|
#include "GPSStatus.h"
|
||||||
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "NodeListRenderer.h"
|
#include "NodeListRenderer.h"
|
||||||
#include "UIRenderer.h"
|
#include "UIRenderer.h"
|
||||||
@@ -313,7 +314,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
|||||||
// === Create the shortName and title string ===
|
// === Create the shortName and title string ===
|
||||||
const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node";
|
const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node";
|
||||||
char titlestr[32] = {0};
|
char titlestr[32] = {0};
|
||||||
snprintf(titlestr, sizeof(titlestr), "Fav: %s", shortName);
|
snprintf(titlestr, sizeof(titlestr), "*%s*", shortName);
|
||||||
|
|
||||||
// === Draw battery/time/mail header (common across screens) ===
|
// === Draw battery/time/mail header (common across screens) ===
|
||||||
graphics::drawCommonHeader(display, x, y, titlestr);
|
graphics::drawCommonHeader(display, x, y, titlestr);
|
||||||
@@ -342,34 +343,162 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === 2. Signal and Hops (combined on one line, if available) ===
|
// === 2. Signal and Hops (combined on one line, if available) ===
|
||||||
// If both are present: "Sig: 97% [2hops]"
|
|
||||||
// If only one: show only that one
|
|
||||||
char signalHopsStr[32] = "";
|
char signalHopsStr[32] = "";
|
||||||
bool haveSignal = false;
|
bool haveSignal = false;
|
||||||
int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100);
|
int bars = 0;
|
||||||
|
|
||||||
// Always use "Sig" for the label
|
// Helper to get SNR limit based on modem preset
|
||||||
const char *signalLabel = " Sig";
|
auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float {
|
||||||
|
switch (preset) {
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST:
|
||||||
|
return -6.0f;
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
|
||||||
|
return -5.5f;
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
|
||||||
|
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
|
||||||
|
return -4.5f;
|
||||||
|
default:
|
||||||
|
return -6.0f;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate signal grade using modem preset and SNR only
|
||||||
|
float snrLimit = getSnrLimit(config.lora.modem_preset);
|
||||||
|
float snr = node->snr;
|
||||||
|
|
||||||
|
// Determine signal quality label and bars using SNR-only grading
|
||||||
|
const char *qualityLabel = nullptr;
|
||||||
|
|
||||||
|
if (snr > snrLimit + 10) {
|
||||||
|
qualityLabel = "Good";
|
||||||
|
bars = 4;
|
||||||
|
} else if (snr > snrLimit + 6) {
|
||||||
|
qualityLabel = "Good";
|
||||||
|
bars = 3;
|
||||||
|
} else if (snr > snrLimit + 2) {
|
||||||
|
qualityLabel = "Good";
|
||||||
|
bars = 2;
|
||||||
|
} else if (snr > snrLimit - 4) {
|
||||||
|
qualityLabel = "Fair";
|
||||||
|
bars = 1;
|
||||||
|
} else {
|
||||||
|
qualityLabel = "Bad";
|
||||||
|
bars = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add extra spacing on the left if we have an API connection to account for the common footer icons
|
||||||
|
const char *leftSideSpacing =
|
||||||
|
graphics::isAPIConnected(service->api_state) ? (currentResolution == ScreenResolution::High ? " " : " ") : " ";
|
||||||
|
|
||||||
// --- Build the Signal/Hops line ---
|
// --- Build the Signal/Hops line ---
|
||||||
// If SNR looks reasonable, show signal
|
// Only show signal if we have valid SNR
|
||||||
if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) {
|
if (snr > -100 && snr != 0) {
|
||||||
snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal);
|
snprintf(signalHopsStr, sizeof(signalHopsStr), "%sSig:%s", leftSideSpacing, qualityLabel);
|
||||||
haveSignal = true;
|
haveSignal = true;
|
||||||
}
|
}
|
||||||
// If hops is valid (>0), show right after signal
|
|
||||||
if (node->hops_away > 0) {
|
if (node->hops_away > 0) {
|
||||||
size_t len = strlen(signalHopsStr);
|
size_t len = strlen(signalHopsStr);
|
||||||
// Decide between "1 Hop" and "N Hops"
|
|
||||||
if (haveSignal) {
|
if (haveSignal) {
|
||||||
snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away,
|
snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [#]");
|
||||||
(node->hops_away == 1 ? "Hop" : "Hops"));
|
|
||||||
} else {
|
} else {
|
||||||
snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops"));
|
snprintf(signalHopsStr, sizeof(signalHopsStr), "[#]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (signalHopsStr[0] && line < 5) {
|
|
||||||
display->drawString(x, getTextPositions(display)[line++], signalHopsStr);
|
if (signalHopsStr[0]) {
|
||||||
|
int yPos = getTextPositions(display)[line++];
|
||||||
|
int curX = x;
|
||||||
|
|
||||||
|
// Split combined string into signal text and hop suffix
|
||||||
|
char sigPart[20] = "";
|
||||||
|
const char *hopPart = nullptr;
|
||||||
|
|
||||||
|
char *bracket = strchr(signalHopsStr, '[');
|
||||||
|
if (bracket) {
|
||||||
|
size_t n = (size_t)(bracket - signalHopsStr);
|
||||||
|
if (n >= sizeof(sigPart))
|
||||||
|
n = sizeof(sigPart) - 1;
|
||||||
|
memcpy(sigPart, signalHopsStr, n);
|
||||||
|
sigPart[n] = '\0';
|
||||||
|
|
||||||
|
// Trim trailing spaces
|
||||||
|
while (strlen(sigPart) && sigPart[strlen(sigPart) - 1] == ' ') {
|
||||||
|
sigPart[strlen(sigPart) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
hopPart = bracket; // "[n Hop(s)]"
|
||||||
|
} else {
|
||||||
|
strncpy(sigPart, signalHopsStr, sizeof(sigPart) - 1);
|
||||||
|
sigPart[sizeof(sigPart) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw signal quality text
|
||||||
|
display->drawString(curX, yPos, sigPart);
|
||||||
|
curX += display->getStringWidth(sigPart) + 4;
|
||||||
|
|
||||||
|
// Draw signal bars (skip on UltraLow, text only)
|
||||||
|
if (currentResolution != ScreenResolution::UltraLow && haveSignal && bars > 0) {
|
||||||
|
const int kMaxBars = 4;
|
||||||
|
if (bars < 1)
|
||||||
|
bars = 1;
|
||||||
|
if (bars > kMaxBars)
|
||||||
|
bars = kMaxBars;
|
||||||
|
|
||||||
|
int barX = curX;
|
||||||
|
|
||||||
|
const bool hi = (currentResolution == ScreenResolution::High);
|
||||||
|
int barWidth = hi ? 2 : 1;
|
||||||
|
int barGap = hi ? 2 : 1;
|
||||||
|
int maxBarHeight = FONT_HEIGHT_SMALL - 7;
|
||||||
|
if (!hi)
|
||||||
|
maxBarHeight -= 1;
|
||||||
|
int barY = yPos + (FONT_HEIGHT_SMALL - maxBarHeight) / 2;
|
||||||
|
|
||||||
|
for (int bi = 0; bi < kMaxBars; bi++) {
|
||||||
|
int barHeight = maxBarHeight * (bi + 1) / kMaxBars;
|
||||||
|
if (barHeight < 2)
|
||||||
|
barHeight = 2;
|
||||||
|
|
||||||
|
int bx = barX + bi * (barWidth + barGap);
|
||||||
|
int by = barY + maxBarHeight - barHeight;
|
||||||
|
|
||||||
|
if (bi < bars) {
|
||||||
|
display->fillRect(bx, by, barWidth, barHeight);
|
||||||
|
} else {
|
||||||
|
int baseY = barY + maxBarHeight - 1;
|
||||||
|
display->drawHorizontalLine(bx, baseY, barWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
curX += (kMaxBars * barWidth) + ((kMaxBars - 1) * barGap) + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw hops AFTER the bars as: [ number + hop icon ]
|
||||||
|
if (hopPart && node->hops_away > 0) {
|
||||||
|
|
||||||
|
// open bracket
|
||||||
|
display->drawString(curX, yPos, "[");
|
||||||
|
curX += display->getStringWidth("[") + 1;
|
||||||
|
|
||||||
|
// hop count
|
||||||
|
char hopCount[6];
|
||||||
|
snprintf(hopCount, sizeof(hopCount), "%d", node->hops_away);
|
||||||
|
display->drawString(curX, yPos, hopCount);
|
||||||
|
curX += display->getStringWidth(hopCount) + 2;
|
||||||
|
|
||||||
|
// hop icon
|
||||||
|
const int iconY = yPos + (FONT_HEIGHT_SMALL - hop_height) / 2;
|
||||||
|
display->drawXbm(curX, iconY, hop_width, hop_height, hop);
|
||||||
|
curX += hop_width + 1;
|
||||||
|
|
||||||
|
// closing bracket
|
||||||
|
display->drawString(curX, yPos, "]");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 3. Heard (last seen, skip if node never seen) ===
|
// === 3. Heard (last seen, skip if node never seen) ===
|
||||||
@@ -377,8 +506,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
|||||||
uint32_t seconds = sinceLastSeen(node);
|
uint32_t seconds = sinceLastSeen(node);
|
||||||
if (seconds != 0 && seconds != UINT32_MAX) {
|
if (seconds != 0 && seconds != UINT32_MAX) {
|
||||||
uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
|
uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
|
||||||
// Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago"
|
// Format as "Heard:Xm ago", "Heard:Xh ago", or "Heard:Xd ago"
|
||||||
snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"),
|
snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard:?" : "%sHeard:%d%c ago"), leftSideSpacing,
|
||||||
(days ? days
|
(days ? days
|
||||||
: hours ? hours
|
: hours ? hours
|
||||||
: minutes),
|
: minutes),
|
||||||
@@ -386,16 +515,18 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
|||||||
: hours ? 'h'
|
: hours ? 'h'
|
||||||
: 'm'));
|
: 'm'));
|
||||||
}
|
}
|
||||||
if (seenStr[0] && line < 5) {
|
if (seenStr[0]) {
|
||||||
display->drawString(x, getTextPositions(display)[line++], seenStr);
|
display->drawString(x, getTextPositions(display)[line++], seenStr);
|
||||||
}
|
}
|
||||||
#if !defined(M5STACK_UNITC6L)
|
#if !defined(M5STACK_UNITC6L)
|
||||||
// === 4. Uptime (only show if metric is present) ===
|
// === 4. Uptime (only show if metric is present) ===
|
||||||
char uptimeStr[32] = "";
|
char uptimeStr[32] = "";
|
||||||
if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) {
|
if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) {
|
||||||
getUptimeStr(node->device_metrics.uptime_seconds * 1000, " Up", uptimeStr, sizeof(uptimeStr));
|
char upPrefix[12]; // enough for leftSideSpacing + "Up:"
|
||||||
|
snprintf(upPrefix, sizeof(upPrefix), "%sUp:", leftSideSpacing);
|
||||||
|
getUptimeStr(node->device_metrics.uptime_seconds * 1000, upPrefix, uptimeStr, sizeof(uptimeStr));
|
||||||
}
|
}
|
||||||
if (uptimeStr[0] && line < 5) {
|
if (uptimeStr[0]) {
|
||||||
display->drawString(x, getTextPositions(display)[line++], uptimeStr);
|
display->drawString(x, getTextPositions(display)[line++], uptimeStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,16 +553,16 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
|||||||
if (miles < 0.1) {
|
if (miles < 0.1) {
|
||||||
int feet = (int)(miles * 5280);
|
int feet = (int)(miles * 5280);
|
||||||
if (feet > 0 && feet < 1000) {
|
if (feet > 0 && feet < 1000) {
|
||||||
snprintf(distStr, sizeof(distStr), " Distance: %dft", feet);
|
snprintf(distStr, sizeof(distStr), "%sDistance:%dft", leftSideSpacing, feet);
|
||||||
haveDistance = true;
|
haveDistance = true;
|
||||||
} else if (feet >= 1000) {
|
} else if (feet >= 1000) {
|
||||||
snprintf(distStr, sizeof(distStr), " Distance: ¼mi");
|
snprintf(distStr, sizeof(distStr), "%sDistance:¼mi", leftSideSpacing);
|
||||||
haveDistance = true;
|
haveDistance = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
int roundedMiles = (int)(miles + 0.5);
|
int roundedMiles = (int)(miles + 0.5);
|
||||||
if (roundedMiles > 0 && roundedMiles < 1000) {
|
if (roundedMiles > 0 && roundedMiles < 1000) {
|
||||||
snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles);
|
snprintf(distStr, sizeof(distStr), "%sDistance:%dmi", leftSideSpacing, roundedMiles);
|
||||||
haveDistance = true;
|
haveDistance = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,26 +570,74 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
|||||||
if (distanceKm < 1.0) {
|
if (distanceKm < 1.0) {
|
||||||
int meters = (int)(distanceKm * 1000);
|
int meters = (int)(distanceKm * 1000);
|
||||||
if (meters > 0 && meters < 1000) {
|
if (meters > 0 && meters < 1000) {
|
||||||
snprintf(distStr, sizeof(distStr), " Distance: %dm", meters);
|
snprintf(distStr, sizeof(distStr), "%sDistance:%dm", leftSideSpacing, meters);
|
||||||
haveDistance = true;
|
haveDistance = true;
|
||||||
} else if (meters >= 1000) {
|
} else if (meters >= 1000) {
|
||||||
snprintf(distStr, sizeof(distStr), " Distance: 1km");
|
snprintf(distStr, sizeof(distStr), "%sDistance:1km", leftSideSpacing);
|
||||||
haveDistance = true;
|
haveDistance = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
int km = (int)(distanceKm + 0.5);
|
int km = (int)(distanceKm + 0.5);
|
||||||
if (km > 0 && km < 1000) {
|
if (km > 0 && km < 1000) {
|
||||||
snprintf(distStr, sizeof(distStr), " Distance: %dkm", km);
|
snprintf(distStr, sizeof(distStr), "%sDistance:%dkm", leftSideSpacing, km);
|
||||||
haveDistance = true;
|
haveDistance = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Only display if we actually have a value!
|
if (haveDistance && distStr[0]) {
|
||||||
if (haveDistance && distStr[0] && line < 5) {
|
|
||||||
display->drawString(x, getTextPositions(display)[line++], distStr);
|
display->drawString(x, getTextPositions(display)[line++], distStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === 6. Battery after Distance line, otherwise next available line ===
|
||||||
|
char batLine[32] = "";
|
||||||
|
bool haveBatLine = false;
|
||||||
|
|
||||||
|
if (node->has_device_metrics) {
|
||||||
|
bool hasPct = node->device_metrics.has_battery_level;
|
||||||
|
bool hasVolt = node->device_metrics.has_voltage && node->device_metrics.voltage > 0.001f;
|
||||||
|
|
||||||
|
int pct = 0;
|
||||||
|
float volt = 0.0f;
|
||||||
|
|
||||||
|
if (hasPct) {
|
||||||
|
pct = (int)node->device_metrics.battery_level;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasVolt) {
|
||||||
|
volt = node->device_metrics.voltage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPct && pct > 0 && pct <= 100) {
|
||||||
|
// Normal battery percentage
|
||||||
|
if (hasVolt) {
|
||||||
|
snprintf(batLine, sizeof(batLine), "%sBat:%d%% (%.2fV)", leftSideSpacing, pct, volt);
|
||||||
|
} else {
|
||||||
|
snprintf(batLine, sizeof(batLine), "%sBat:%d%%", leftSideSpacing, pct);
|
||||||
|
}
|
||||||
|
haveBatLine = true;
|
||||||
|
} else if (hasPct && pct > 100) {
|
||||||
|
// Plugged in
|
||||||
|
if (hasVolt) {
|
||||||
|
snprintf(batLine, sizeof(batLine), "%sPlugged In (%.2fV)", leftSideSpacing, volt);
|
||||||
|
} else {
|
||||||
|
snprintf(batLine, sizeof(batLine), "%sPlugged In", leftSideSpacing);
|
||||||
|
}
|
||||||
|
haveBatLine = true;
|
||||||
|
} else if (!hasPct && hasVolt) {
|
||||||
|
// Voltage only
|
||||||
|
snprintf(batLine, sizeof(batLine), "%sBat:%.2fV", leftSideSpacing, volt);
|
||||||
|
haveBatLine = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int maxTextLines = (currentResolution == ScreenResolution::High) ? 6 : 5;
|
||||||
|
|
||||||
|
// Only draw battery if it fits within the allowed lines
|
||||||
|
if (haveBatLine && line <= maxTextLines) {
|
||||||
|
display->drawString(x, getTextPositions(display)[line++], batLine);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Compass Rendering: landscape (wide) screens use the original side-aligned logic ---
|
// --- Compass Rendering: landscape (wide) screens use the original side-aligned logic ---
|
||||||
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
|
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
|
||||||
bool showCompass = false;
|
bool showCompass = false;
|
||||||
@@ -593,7 +772,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
|||||||
}
|
}
|
||||||
char uptimeStr[32] = "";
|
char uptimeStr[32] = "";
|
||||||
if (currentResolution != ScreenResolution::UltraLow) {
|
if (currentResolution != ScreenResolution::UltraLow) {
|
||||||
getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr));
|
getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr));
|
||||||
}
|
}
|
||||||
display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr);
|
display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr);
|
||||||
|
|
||||||
@@ -984,7 +1163,6 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
config.display.heading_bold = false;
|
config.display.heading_bold = false;
|
||||||
|
|
||||||
const char *displayLine = ""; // Initialize to empty string by default
|
const char *displayLine = ""; // Initialize to empty string by default
|
||||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
|
||||||
|
|
||||||
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||||
if (config.position.fixed_position) {
|
if (config.position.fixed_position) {
|
||||||
@@ -1029,10 +1207,10 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
char uptimeStr[32];
|
char uptimeStr[32];
|
||||||
#if defined(USE_EINK)
|
#if defined(USE_EINK)
|
||||||
// E-Ink: skip seconds, show only days/hours/mins
|
// E-Ink: skip seconds, show only days/hours/mins
|
||||||
getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), false);
|
getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), false);
|
||||||
#else
|
#else
|
||||||
// Non E-Ink: include seconds where useful
|
// Non E-Ink: include seconds where useful
|
||||||
getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), true);
|
getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), true);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
display->drawString(0, getTextPositions(display)[line++], uptimeStr);
|
display->drawString(0, getTextPositions(display)[line++], uptimeStr);
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ static const unsigned char mail[] PROGMEM = {
|
|||||||
0b11111111, 0b00 // Bottom line
|
0b11111111, 0b00 // Bottom line
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Hop icon (9x10)
|
||||||
|
#define hop_width 9
|
||||||
|
#define hop_height 10
|
||||||
|
const uint8_t hop[] PROGMEM = {0x05, 0x00, 0x07, 0x00, 0x05, 0x00, 0x38, 0x00, 0x28, 0x00,
|
||||||
|
0x38, 0x00, 0xC0, 0x01, 0x40, 0x01, 0xC0, 0x01, 0x40, 0x00};
|
||||||
|
|
||||||
// 📬 Mail / Message
|
// 📬 Mail / Message
|
||||||
const uint8_t icon_mail[] PROGMEM = {
|
const uint8_t icon_mail[] PROGMEM = {
|
||||||
0b11111111, // ████████ top border
|
0b11111111, // ████████ top border
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -176,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,7 +21,7 @@ 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;
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ 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,
|
||||||
BACK,
|
|
||||||
NEXT_TILE,
|
NEXT_TILE,
|
||||||
TOGGLE_BACKLIGHT,
|
TOGGLE_BACKLIGHT,
|
||||||
TOGGLE_GPS,
|
TOGGLE_GPS,
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ void InkHUD::MenuApplet::onForeground()
|
|||||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||||
OSThread::enabled = true;
|
OSThread::enabled = true;
|
||||||
|
|
||||||
|
freeTextMode = false;
|
||||||
|
|
||||||
// Upgrade the refresh to FAST, for guaranteed responsiveness
|
// Upgrade the refresh to FAST, for guaranteed responsiveness
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||||
}
|
}
|
||||||
@@ -116,6 +118,8 @@ void InkHUD::MenuApplet::onBackground()
|
|||||||
SystemApplet::lockRequests = false;
|
SystemApplet::lockRequests = false;
|
||||||
SystemApplet::handleInput = false;
|
SystemApplet::handleInput = false;
|
||||||
|
|
||||||
|
handleFreeText = false;
|
||||||
|
|
||||||
// Restore the user applet whose tile we borrowed
|
// Restore the user applet whose tile we borrowed
|
||||||
if (borrowedTileOwner)
|
if (borrowedTileOwner)
|
||||||
borrowedTileOwner->bringToForeground();
|
borrowedTileOwner->bringToForeground();
|
||||||
@@ -325,10 +329,6 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case BACK:
|
|
||||||
showPage(item.nextPage);
|
|
||||||
return;
|
|
||||||
|
|
||||||
case NEXT_TILE:
|
case NEXT_TILE:
|
||||||
inkhud->nextTile();
|
inkhud->nextTile();
|
||||||
// Unselect menu item after tile change
|
// Unselect menu item after tile change
|
||||||
@@ -344,12 +344,26 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
|||||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL);
|
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case FREE_TEXT:
|
||||||
|
OSThread::enabled = false;
|
||||||
|
handleFreeText = true;
|
||||||
|
cm.freeTextItem.rawText.erase(); // clear the previous freetext message
|
||||||
|
freeTextMode = true; // render input field instead of normal menu
|
||||||
|
// Open the on-screen keyboard if the joystick is enabled
|
||||||
|
if (settings->joystick.enabled)
|
||||||
|
inkhud->openKeyboard();
|
||||||
|
break;
|
||||||
|
|
||||||
case STORE_CANNEDMESSAGE_SELECTION:
|
case STORE_CANNEDMESSAGE_SELECTION:
|
||||||
cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
|
if (!settings->joystick.enabled)
|
||||||
|
cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
|
||||||
|
else
|
||||||
|
cm.selectedMessageItem = &cm.messageItems.at(cursor - 2); // Minus two: offset for the "Send Ping" and free text entry
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SEND_CANNEDMESSAGE:
|
case SEND_CANNEDMESSAGE:
|
||||||
cm.selectedRecipientItem = &cm.recipientItems.at(cursor);
|
cm.selectedRecipientItem = &cm.recipientItems.at(cursor);
|
||||||
|
// send selected message
|
||||||
sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str());
|
sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str());
|
||||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here
|
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here
|
||||||
break;
|
break;
|
||||||
@@ -868,6 +882,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
|
|
||||||
switch (page) {
|
switch (page) {
|
||||||
case ROOT:
|
case ROOT:
|
||||||
|
previousPage = MenuPage::EXIT;
|
||||||
// Optional: next applet
|
// Optional: next applet
|
||||||
if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1)
|
if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1)
|
||||||
items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown
|
items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown
|
||||||
@@ -878,7 +893,6 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
items.push_back(MenuItem("Node Config", MenuPage::NODE_CONFIG));
|
items.push_back(MenuItem("Node Config", MenuPage::NODE_CONFIG));
|
||||||
items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN));
|
items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN));
|
||||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||||
previousPage = MenuPage::EXIT;
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SEND:
|
case SEND:
|
||||||
@@ -888,11 +902,12 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
|
|
||||||
case CANNEDMESSAGE_RECIPIENT:
|
case CANNEDMESSAGE_RECIPIENT:
|
||||||
populateRecipientPage();
|
populateRecipientPage();
|
||||||
previousPage = MenuPage::OPTIONS;
|
previousPage = MenuPage::SEND;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case OPTIONS:
|
case OPTIONS:
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::ROOT));
|
previousPage = MenuPage::ROOT;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
// Optional: backlight
|
// Optional: backlight
|
||||||
if (settings->optionalMenuItems.backlight)
|
if (settings->optionalMenuItems.backlight)
|
||||||
items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label
|
items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label
|
||||||
@@ -916,31 +931,32 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
invertedColors = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
|
invertedColors = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
|
||||||
items.push_back(MenuItem("Invert Color", MenuAction::TOGGLE_INVERT_COLOR, MenuPage::OPTIONS, &invertedColors));
|
items.push_back(MenuItem("Invert Color", MenuAction::TOGGLE_INVERT_COLOR, MenuPage::OPTIONS, &invertedColors));
|
||||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||||
previousPage = MenuPage::ROOT;
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case APPLETS:
|
case APPLETS:
|
||||||
populateAppletPage(); // must be first
|
|
||||||
items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS));
|
|
||||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
|
||||||
previousPage = MenuPage::OPTIONS;
|
previousPage = MenuPage::OPTIONS;
|
||||||
|
populateAppletPage(); // must be first
|
||||||
|
items.insert(items.begin(), MenuItem("Back", previousPage));
|
||||||
|
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AUTOSHOW:
|
case AUTOSHOW:
|
||||||
populateAutoshowPage(); // must be first
|
|
||||||
items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS));
|
|
||||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
|
||||||
previousPage = MenuPage::OPTIONS;
|
previousPage = MenuPage::OPTIONS;
|
||||||
|
populateAutoshowPage(); // must be first
|
||||||
|
items.insert(items.begin(), MenuItem("Back", previousPage));
|
||||||
|
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case RECENTS:
|
case RECENTS:
|
||||||
|
previousPage = MenuPage::OPTIONS;
|
||||||
populateRecentsPage(); // builds only the options
|
populateRecentsPage(); // builds only the options
|
||||||
items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS));
|
items.insert(items.begin(), MenuItem("Back", previousPage));
|
||||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NODE_CONFIG:
|
case NODE_CONFIG:
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::ROOT));
|
previousPage = MenuPage::ROOT;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
// Radio Config Section
|
// Radio Config Section
|
||||||
items.push_back(MenuItem::Header("Radio Config"));
|
items.push_back(MenuItem::Header("Radio Config"));
|
||||||
items.push_back(MenuItem("LoRa", MenuPage::NODE_CONFIG_LORA));
|
items.push_back(MenuItem("LoRa", MenuPage::NODE_CONFIG_LORA));
|
||||||
@@ -965,8 +981,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case NODE_CONFIG_DEVICE: {
|
case NODE_CONFIG_DEVICE: {
|
||||||
|
previousPage = MenuPage::NODE_CONFIG;
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
|
|
||||||
const char *role = DisplayFormatters::getDeviceRole(config.device.role);
|
const char *role = DisplayFormatters::getDeviceRole(config.device.role);
|
||||||
nodeConfigLabels.emplace_back("Role: " + std::string(role));
|
nodeConfigLabels.emplace_back("Role: " + std::string(role));
|
||||||
@@ -981,7 +997,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case NODE_CONFIG_POSITION: {
|
case NODE_CONFIG_POSITION: {
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
previousPage = MenuPage::NODE_CONFIG;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
#if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS
|
#if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS
|
||||||
const auto mode = config.position.gps_mode;
|
const auto mode = config.position.gps_mode;
|
||||||
if (mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) {
|
if (mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) {
|
||||||
@@ -996,7 +1013,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case NODE_CONFIG_POWER: {
|
case NODE_CONFIG_POWER: {
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
previousPage = MenuPage::NODE_CONFIG;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
#if defined(ARCH_ESP32)
|
#if defined(ARCH_ESP32)
|
||||||
items.push_back(MenuItem("Powersave", MenuAction::TOGGLE_POWER_SAVE, MenuPage::EXIT, &config.power.is_power_saving));
|
items.push_back(MenuItem("Powersave", MenuAction::TOGGLE_POWER_SAVE, MenuPage::EXIT, &config.power.is_power_saving));
|
||||||
#endif
|
#endif
|
||||||
@@ -1029,7 +1047,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case NODE_CONFIG_POWER_ADC_CAL: {
|
case NODE_CONFIG_POWER_ADC_CAL: {
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_POWER));
|
previousPage = MenuPage::NODE_CONFIG_POWER;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
|
|
||||||
// Instruction text (header-style, non-selectable)
|
// Instruction text (header-style, non-selectable)
|
||||||
items.push_back(MenuItem::Header("Run on full charge Only"));
|
items.push_back(MenuItem::Header("Run on full charge Only"));
|
||||||
@@ -1042,7 +1061,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case NODE_CONFIG_NETWORK: {
|
case NODE_CONFIG_NETWORK: {
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
previousPage = MenuPage::NODE_CONFIG;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
|
|
||||||
const char *wifiLabel = config.network.wifi_enabled ? "WiFi: On" : "WiFi: Off";
|
const char *wifiLabel = config.network.wifi_enabled ? "WiFi: On" : "WiFi: Off";
|
||||||
|
|
||||||
@@ -1099,7 +1119,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case NODE_CONFIG_DISPLAY: {
|
case NODE_CONFIG_DISPLAY: {
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
previousPage = MenuPage::NODE_CONFIG;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
|
|
||||||
items.push_back(MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::NODE_CONFIG_DISPLAY,
|
items.push_back(MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::NODE_CONFIG_DISPLAY,
|
||||||
&config.display.use_12h_clock));
|
&config.display.use_12h_clock));
|
||||||
@@ -1114,7 +1135,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case NODE_CONFIG_BLUETOOTH: {
|
case NODE_CONFIG_BLUETOOTH: {
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
previousPage = MenuPage::NODE_CONFIG;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
|
|
||||||
const char *btLabel = config.bluetooth.enabled ? "Bluetooth: On" : "Bluetooth: Off";
|
const char *btLabel = config.bluetooth.enabled ? "Bluetooth: On" : "Bluetooth: Off";
|
||||||
items.push_back(MenuItem(btLabel, MenuAction::TOGGLE_BLUETOOTH, MenuPage::EXIT));
|
items.push_back(MenuItem(btLabel, MenuAction::TOGGLE_BLUETOOTH, MenuPage::EXIT));
|
||||||
@@ -1127,8 +1149,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case NODE_CONFIG_LORA: {
|
case NODE_CONFIG_LORA: {
|
||||||
|
previousPage = MenuPage::NODE_CONFIG;
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
|
|
||||||
const char *region = myRegion ? myRegion->name : "Unset";
|
const char *region = myRegion ? myRegion->name : "Unset";
|
||||||
nodeConfigLabels.emplace_back("Region: " + std::string(region));
|
nodeConfigLabels.emplace_back("Region: " + std::string(region));
|
||||||
@@ -1150,7 +1172,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case NODE_CONFIG_CHANNELS: {
|
case NODE_CONFIG_CHANNELS: {
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
previousPage = MenuPage::NODE_CONFIG;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
|
|
||||||
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
|
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
|
||||||
meshtastic_Channel &ch = channels.getByIndex(i);
|
meshtastic_Channel &ch = channels.getByIndex(i);
|
||||||
@@ -1181,7 +1204,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case NODE_CONFIG_CHANNEL_DETAIL: {
|
case NODE_CONFIG_CHANNEL_DETAIL: {
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_CHANNELS));
|
previousPage = MenuPage::NODE_CONFIG_CHANNELS;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
|
|
||||||
meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
|
meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
|
||||||
|
|
||||||
@@ -1226,7 +1250,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case NODE_CONFIG_CHANNEL_PRECISION: {
|
case NODE_CONFIG_CHANNEL_PRECISION: {
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_CHANNEL_DETAIL));
|
previousPage = MenuPage::NODE_CONFIG_CHANNEL_DETAIL;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
|
meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
|
||||||
if (!ch.settings.has_module_settings || ch.settings.module_settings.position_precision == 0) {
|
if (!ch.settings.has_module_settings || ch.settings.module_settings.position_precision == 0) {
|
||||||
items.push_back(MenuItem("Position is Off", MenuPage::NODE_CONFIG_CHANNEL_DETAIL));
|
items.push_back(MenuItem("Position is Off", MenuPage::NODE_CONFIG_CHANNEL_DETAIL));
|
||||||
@@ -1247,7 +1272,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case NODE_CONFIG_DEVICE_ROLE: {
|
case NODE_CONFIG_DEVICE_ROLE: {
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_DEVICE));
|
previousPage = MenuPage::NODE_CONFIG_DEVICE;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
items.push_back(MenuItem("Client", MenuAction::SET_ROLE_CLIENT, MenuPage::EXIT));
|
items.push_back(MenuItem("Client", MenuAction::SET_ROLE_CLIENT, MenuPage::EXIT));
|
||||||
items.push_back(MenuItem("Client Mute", MenuAction::SET_ROLE_CLIENT_MUTE, MenuPage::EXIT));
|
items.push_back(MenuItem("Client Mute", MenuAction::SET_ROLE_CLIENT_MUTE, MenuPage::EXIT));
|
||||||
items.push_back(MenuItem("Router", MenuAction::SET_ROLE_ROUTER, MenuPage::EXIT));
|
items.push_back(MenuItem("Router", MenuAction::SET_ROLE_ROUTER, MenuPage::EXIT));
|
||||||
@@ -1257,7 +1283,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
}
|
}
|
||||||
|
|
||||||
case TIMEZONE:
|
case TIMEZONE:
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_DEVICE));
|
previousPage = MenuPage::NODE_CONFIG_DEVICE;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
items.push_back(MenuItem("US/Hawaii", SET_TZ_US_HAWAII, MenuPage::NODE_CONFIG_DEVICE));
|
items.push_back(MenuItem("US/Hawaii", SET_TZ_US_HAWAII, MenuPage::NODE_CONFIG_DEVICE));
|
||||||
items.push_back(MenuItem("US/Alaska", SET_TZ_US_ALASKA, MenuPage::NODE_CONFIG_DEVICE));
|
items.push_back(MenuItem("US/Alaska", SET_TZ_US_ALASKA, MenuPage::NODE_CONFIG_DEVICE));
|
||||||
items.push_back(MenuItem("US/Pacific", SET_TZ_US_PACIFIC, MenuPage::NODE_CONFIG_DEVICE));
|
items.push_back(MenuItem("US/Pacific", SET_TZ_US_PACIFIC, MenuPage::NODE_CONFIG_DEVICE));
|
||||||
@@ -1279,7 +1306,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case REGION:
|
case REGION:
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_LORA));
|
previousPage = MenuPage::NODE_CONFIG_LORA;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
items.push_back(MenuItem("US", MenuAction::SET_REGION_US, MenuPage::EXIT));
|
items.push_back(MenuItem("US", MenuAction::SET_REGION_US, MenuPage::EXIT));
|
||||||
items.push_back(MenuItem("EU 868", MenuAction::SET_REGION_EU_868, MenuPage::EXIT));
|
items.push_back(MenuItem("EU 868", MenuAction::SET_REGION_EU_868, MenuPage::EXIT));
|
||||||
items.push_back(MenuItem("EU 433", MenuAction::SET_REGION_EU_433, MenuPage::EXIT));
|
items.push_back(MenuItem("EU 433", MenuAction::SET_REGION_EU_433, MenuPage::EXIT));
|
||||||
@@ -1310,7 +1338,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case NODE_CONFIG_PRESET: {
|
case NODE_CONFIG_PRESET: {
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_LORA));
|
previousPage = MenuPage::NODE_CONFIG_LORA;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
items.push_back(MenuItem("Long Moderate", MenuAction::SET_PRESET_LONG_MODERATE, MenuPage::EXIT));
|
items.push_back(MenuItem("Long Moderate", MenuAction::SET_PRESET_LONG_MODERATE, MenuPage::EXIT));
|
||||||
items.push_back(MenuItem("Long Fast", MenuAction::SET_PRESET_LONG_FAST, MenuPage::EXIT));
|
items.push_back(MenuItem("Long Fast", MenuAction::SET_PRESET_LONG_FAST, MenuPage::EXIT));
|
||||||
items.push_back(MenuItem("Medium Slow", MenuAction::SET_PRESET_MEDIUM_SLOW, MenuPage::EXIT));
|
items.push_back(MenuItem("Medium Slow", MenuAction::SET_PRESET_MEDIUM_SLOW, MenuPage::EXIT));
|
||||||
@@ -1323,7 +1352,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
}
|
}
|
||||||
// Administration Section
|
// Administration Section
|
||||||
case NODE_CONFIG_ADMIN_RESET:
|
case NODE_CONFIG_ADMIN_RESET:
|
||||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
previousPage = MenuPage::NODE_CONFIG;
|
||||||
|
items.push_back(MenuItem("Back", previousPage));
|
||||||
items.push_back(MenuItem("Reset All", MenuAction::RESET_NODEDB_ALL, MenuPage::EXIT));
|
items.push_back(MenuItem("Reset All", MenuAction::RESET_NODEDB_ALL, MenuPage::EXIT));
|
||||||
items.push_back(MenuItem("Keep Favorites Only", MenuAction::RESET_NODEDB_KEEP_FAVORITES, MenuPage::EXIT));
|
items.push_back(MenuItem("Keep Favorites Only", MenuAction::RESET_NODEDB_KEEP_FAVORITES, MenuPage::EXIT));
|
||||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||||
@@ -1361,8 +1391,14 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
currentPage = page;
|
currentPage = page;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::MenuApplet::onRender()
|
void InkHUD::MenuApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
|
// Free text mode draws a text input field and skips the normal rendering
|
||||||
|
if (freeTextMode) {
|
||||||
|
drawInputField(0, fontSmall.lineHeight(), X(1.0), Y(1.0) - fontSmall.lineHeight() - 1, cm.freeTextItem.rawText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (items.size() == 0)
|
if (items.size() == 0)
|
||||||
LOG_ERROR("Empty Menu");
|
LOG_ERROR("Empty Menu");
|
||||||
|
|
||||||
@@ -1481,44 +1517,48 @@ void InkHUD::MenuApplet::onRender()
|
|||||||
|
|
||||||
void InkHUD::MenuApplet::onButtonShortPress()
|
void InkHUD::MenuApplet::onButtonShortPress()
|
||||||
{
|
{
|
||||||
// Push the auto-close timer back
|
if (!freeTextMode) {
|
||||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
// Push the auto-close timer back
|
||||||
|
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||||
|
|
||||||
if (!settings->joystick.enabled) {
|
if (!settings->joystick.enabled) {
|
||||||
if (!cursorShown) {
|
if (!cursorShown) {
|
||||||
cursorShown = true;
|
cursorShown = true;
|
||||||
cursor = 0;
|
cursor = 0;
|
||||||
} else {
|
} else {
|
||||||
do {
|
do {
|
||||||
cursor = (cursor + 1) % items.size();
|
cursor = (cursor + 1) % items.size();
|
||||||
} while (items.at(cursor).isHeader);
|
} while (items.at(cursor).isHeader);
|
||||||
}
|
}
|
||||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
|
||||||
} else {
|
|
||||||
if (cursorShown)
|
|
||||||
execute(items.at(cursor));
|
|
||||||
else
|
|
||||||
showPage(MenuPage::EXIT);
|
|
||||||
if (!wantsToRender())
|
|
||||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||||
|
} else {
|
||||||
|
if (cursorShown)
|
||||||
|
execute(items.at(cursor));
|
||||||
|
else
|
||||||
|
showPage(MenuPage::EXIT);
|
||||||
|
if (!wantsToRender())
|
||||||
|
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::MenuApplet::onButtonLongPress()
|
void InkHUD::MenuApplet::onButtonLongPress()
|
||||||
{
|
{
|
||||||
// Push the auto-close timer back
|
if (!freeTextMode) {
|
||||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
// Push the auto-close timer back
|
||||||
|
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||||
|
|
||||||
if (cursorShown)
|
if (cursorShown)
|
||||||
execute(items.at(cursor));
|
execute(items.at(cursor));
|
||||||
else
|
else
|
||||||
showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close
|
showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close
|
||||||
|
|
||||||
// If we didn't already request a specialized update, when handling a menu action,
|
// If we didn't already request a specialized update, when handling a menu action,
|
||||||
// then perform the usual fast update.
|
// then perform the usual fast update.
|
||||||
// FAST keeps things responsive: important because we're dealing with user input
|
// FAST keeps things responsive: important because we're dealing with user input
|
||||||
if (!wantsToRender())
|
if (!wantsToRender())
|
||||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::MenuApplet::onExitShort()
|
void InkHUD::MenuApplet::onExitShort()
|
||||||
@@ -1531,56 +1571,107 @@ void InkHUD::MenuApplet::onExitShort()
|
|||||||
|
|
||||||
void InkHUD::MenuApplet::onNavUp()
|
void InkHUD::MenuApplet::onNavUp()
|
||||||
{
|
{
|
||||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
if (!freeTextMode) {
|
||||||
|
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||||
|
|
||||||
if (!cursorShown) {
|
if (!cursorShown) {
|
||||||
cursorShown = true;
|
cursorShown = true;
|
||||||
cursor = 0;
|
cursor = 0;
|
||||||
} else {
|
} else {
|
||||||
do {
|
do {
|
||||||
if (cursor == 0)
|
if (cursor == 0)
|
||||||
cursor = items.size() - 1;
|
cursor = items.size() - 1;
|
||||||
else
|
else
|
||||||
cursor--;
|
cursor--;
|
||||||
} while (items.at(cursor).isHeader);
|
} while (items.at(cursor).isHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||||
}
|
}
|
||||||
|
|
||||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::MenuApplet::onNavDown()
|
void InkHUD::MenuApplet::onNavDown()
|
||||||
{
|
{
|
||||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
if (!freeTextMode) {
|
||||||
|
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||||
|
|
||||||
if (!cursorShown) {
|
if (!cursorShown) {
|
||||||
cursorShown = true;
|
cursorShown = true;
|
||||||
cursor = 0;
|
cursor = 0;
|
||||||
} else {
|
} else {
|
||||||
do {
|
do {
|
||||||
cursor = (cursor + 1) % items.size();
|
cursor = (cursor + 1) % items.size();
|
||||||
} while (items.at(cursor).isHeader);
|
} while (items.at(cursor).isHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||||
}
|
}
|
||||||
|
|
||||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::MenuApplet::onNavLeft()
|
void InkHUD::MenuApplet::onNavLeft()
|
||||||
{
|
{
|
||||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
if (!freeTextMode) {
|
||||||
|
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||||
|
|
||||||
// Go to the previous menu page
|
// Go to the previous menu page
|
||||||
showPage(previousPage);
|
showPage(previousPage);
|
||||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::MenuApplet::onNavRight()
|
void InkHUD::MenuApplet::onNavRight()
|
||||||
{
|
{
|
||||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
if (!freeTextMode) {
|
||||||
|
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||||
|
if (cursorShown)
|
||||||
|
execute(items.at(cursor));
|
||||||
|
if (!wantsToRender())
|
||||||
|
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (cursorShown)
|
void InkHUD::MenuApplet::onFreeText(char c)
|
||||||
execute(items.at(cursor));
|
{
|
||||||
if (!wantsToRender())
|
if (cm.freeTextItem.rawText.length() >= menuTextLimit && c != '\b')
|
||||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
return;
|
||||||
|
if (c == '\b') {
|
||||||
|
if (!cm.freeTextItem.rawText.empty())
|
||||||
|
cm.freeTextItem.rawText.pop_back();
|
||||||
|
} else {
|
||||||
|
cm.freeTextItem.rawText += c;
|
||||||
|
}
|
||||||
|
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::MenuApplet::onFreeTextDone()
|
||||||
|
{
|
||||||
|
// Restart the auto-close timeout
|
||||||
|
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||||
|
OSThread::enabled = true;
|
||||||
|
|
||||||
|
handleFreeText = false;
|
||||||
|
freeTextMode = false;
|
||||||
|
|
||||||
|
if (!cm.freeTextItem.rawText.empty()) {
|
||||||
|
cm.selectedMessageItem = &cm.freeTextItem;
|
||||||
|
showPage(MenuPage::CANNEDMESSAGE_RECIPIENT);
|
||||||
|
}
|
||||||
|
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InkHUD::MenuApplet::onFreeTextCancel()
|
||||||
|
{
|
||||||
|
// Restart the auto-close timeout
|
||||||
|
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||||
|
OSThread::enabled = true;
|
||||||
|
|
||||||
|
handleFreeText = false;
|
||||||
|
freeTextMode = false;
|
||||||
|
|
||||||
|
// Clear the free text message
|
||||||
|
cm.freeTextItem.rawText.erase();
|
||||||
|
|
||||||
|
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu
|
// Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu
|
||||||
@@ -1635,6 +1726,10 @@ void InkHUD::MenuApplet::populateSendPage()
|
|||||||
// Position / NodeInfo packet
|
// Position / NodeInfo packet
|
||||||
items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT));
|
items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT));
|
||||||
|
|
||||||
|
// If joystick is available, include the Free Text option
|
||||||
|
if (settings->joystick.enabled)
|
||||||
|
items.push_back(MenuItem("Free Text", MenuAction::FREE_TEXT, MenuPage::SEND));
|
||||||
|
|
||||||
// One menu item for each canned message
|
// One menu item for each canned message
|
||||||
uint8_t count = cm.store->size();
|
uint8_t count = cm.store->size();
|
||||||
for (uint8_t i = 0; i < count; i++) {
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
@@ -1734,6 +1829,48 @@ void InkHUD::MenuApplet::populateRecipientPage()
|
|||||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, std::string text)
|
||||||
|
{
|
||||||
|
setFont(fontSmall);
|
||||||
|
uint16_t wrapMaxH = 0;
|
||||||
|
|
||||||
|
// Draw the text, input box, and cursor
|
||||||
|
// Adjusting the box for screen height
|
||||||
|
while (wrapMaxH < height - fontSmall.lineHeight()) {
|
||||||
|
wrapMaxH += fontSmall.lineHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the text is so long that it goes outside of the input box, the text is actually rendered off screen.
|
||||||
|
uint32_t textHeight = getWrappedTextHeight(0, width - 5, text);
|
||||||
|
if (!text.empty()) {
|
||||||
|
uint16_t textPadding = X(1.0) > Y(1.0) ? wrapMaxH - textHeight : wrapMaxH - textHeight + 1;
|
||||||
|
if (textHeight > wrapMaxH)
|
||||||
|
printWrapped(2, textPadding, width - 5, text);
|
||||||
|
else
|
||||||
|
printWrapped(2, top + 2, width - 5, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t textCursorX = text.empty() ? 1 : getCursorX();
|
||||||
|
uint16_t textCursorY = text.empty() ? fontSmall.lineHeight() + 2 : getCursorY() - fontSmall.lineHeight() + 3;
|
||||||
|
|
||||||
|
if (textCursorX + 1 > width - 5) {
|
||||||
|
textCursorX = getCursorX() - width + 5;
|
||||||
|
textCursorY += fontSmall.lineHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
fillRect(textCursorX + 1, textCursorY, 1, fontSmall.lineHeight(), BLACK);
|
||||||
|
|
||||||
|
// A white rectangle clears the top part of the screen for any text that's printed beyond the input box
|
||||||
|
fillRect(0, 0, X(1.0), top, WHITE);
|
||||||
|
|
||||||
|
// Draw character limit
|
||||||
|
std::string ftlen = std::to_string(text.length()) + "/" + to_string(menuTextLimit);
|
||||||
|
uint16_t textLen = getTextWidth(ftlen);
|
||||||
|
printAt(X(1.0) - textLen - 2, 0, ftlen);
|
||||||
|
|
||||||
|
// Draw the border
|
||||||
|
drawRect(0, top, width, wrapMaxH + 5, BLACK);
|
||||||
|
}
|
||||||
// Renders the panel shown at the top of the root menu.
|
// Renders the panel shown at the top of the root menu.
|
||||||
// Displays the clock, and several other pieces of instantaneous system info,
|
// Displays the clock, and several other pieces of instantaneous system info,
|
||||||
// which we'd prefer not to have displayed in a normal applet, as they update too frequently.
|
// which we'd prefer not to have displayed in a normal applet, as they update too frequently.
|
||||||
@@ -1875,4 +2012,4 @@ void InkHUD::MenuApplet::freeCannedMessageResources()
|
|||||||
cm.messageItems.clear();
|
cm.messageItems.clear();
|
||||||
cm.recipientItems.clear();
|
cm.recipientItems.clear();
|
||||||
}
|
}
|
||||||
#endif // MESHTASTIC_INCLUDE_INKHUD
|
#endif // MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ 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);
|
void setStartPage(MenuPage page);
|
||||||
@@ -51,6 +54,8 @@ 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
|
||||||
@@ -62,8 +67,9 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
|||||||
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
|
std::vector<std::string> nodeConfigLabels; // Persistent labels for Node Config pages
|
||||||
@@ -104,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ InkHUD::TipsApplet::TipsApplet()
|
|||||||
bringToForeground();
|
bringToForeground();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::TipsApplet::onRender()
|
void InkHUD::TipsApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
switch (tipQueue.front()) {
|
switch (tipQueue.front()) {
|
||||||
case Tip::WELCOME:
|
case Tip::WELCOME:
|
||||||
@@ -261,7 +261,7 @@ 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
|
||||||
@@ -292,9 +292,8 @@ void InkHUD::TipsApplet::onButtonShortPress()
|
|||||||
inkhud->persistence->saveSettings();
|
inkhud->persistence->saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close applet and clean the screen
|
// Close applet
|
||||||
sendToBackground();
|
sendToBackground();
|
||||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
|
||||||
} else {
|
} else {
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
@@ -306,4 +305,4 @@ void InkHUD::TipsApplet::onExitShort()
|
|||||||
onButtonShortPress();
|
onButtonShortPress();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -33,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;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user