Compare commits
177
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40ef34aa23 | ||
|
|
c8ff02cc35 | ||
|
|
d770ef27c7 | ||
|
|
71edf2fb48 | ||
|
|
c79e1925d9 | ||
|
|
3d649d518f | ||
|
|
947f8176dc | ||
|
|
d5bde83ff5 | ||
|
|
c3321771ef | ||
|
|
b6e5534a8c | ||
|
|
f10d786d82 | ||
|
|
1a7f560372 | ||
|
|
38c7ad0ed6 | ||
|
|
bcc24705a5 | ||
|
|
8137200cb8 | ||
|
|
bfbce2e314 | ||
|
|
f86291546d | ||
|
|
640dd5f7d7 | ||
|
+2 |
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 | ||
|
+1 |
579a0af516 | ||
|
|
73adeee385 | ||
|
|
bb3d6d5326 | ||
|
+1 |
7cbab4838c | ||
|
|
4ce554e09d | ||
|
|
eb145f8adc | ||
|
|
a60e7cfe62 | ||
|
|
53231ae4b1 | ||
|
|
39139cc2ea | ||
|
|
5280caf9d8 | ||
|
|
ba016fd91a | ||
|
|
e2cf401ad3 | ||
|
|
4a4b1f4a87 | ||
|
|
779e446d14 | ||
|
|
f73d18384d | ||
|
|
11bb2ee84e | ||
|
|
2361776992 | ||
|
|
a324c4af10 | ||
|
|
5df5ab2790 | ||
|
|
74ea6206d9 | ||
|
|
b238744445 | ||
|
|
94b7149958 | ||
|
|
ac611c4b62 | ||
|
|
89df5ef669 | ||
|
|
be5f0a9ade | ||
|
|
bfc3eebd54 | ||
|
|
538a5f0dfc | ||
|
|
b7db22055d | ||
|
|
b008c7a170 | ||
|
|
c8a9cdc148 | ||
|
|
0703e0e6d7 | ||
|
|
644fa5b54e | ||
|
|
f514bc230b | ||
|
|
0022148323 | ||
|
|
9d06c1bf34 | ||
|
|
1d30342c00 | ||
|
|
e9d4485bb5 | ||
|
|
7b03980e0a | ||
|
|
004179c045 | ||
|
|
8f630bfcf3 | ||
|
|
7bbfe99fbe | ||
|
|
caae6bc597 | ||
|
|
200e79e800 | ||
|
|
c19fc62683 | ||
|
|
4cf01e7e53 | ||
|
|
ad4b1d9c2b | ||
|
|
68733a6c51 | ||
|
|
22617076f8 | ||
|
|
6f5a7672b4 | ||
|
|
e08c050720 | ||
|
|
28b4f37a93 | ||
|
|
5dd06edd00 | ||
|
|
eeb7373043 | ||
|
|
dbded86dcb | ||
|
|
45fbc0f9d3 | ||
|
|
61b39acc7d | ||
|
|
8af9e7fbdc | ||
|
|
1f7ed6888a | ||
|
|
31bf51b3f2 | ||
|
|
334a4f04cd | ||
|
|
b18742c211 | ||
|
|
03084f6d3b | ||
|
|
94d7b71aa8 | ||
|
|
415686dd06 | ||
|
|
b2f2f6b305 | ||
|
|
df400850c1 | ||
|
|
6ab2f02dbc | ||
|
|
d7d6fe7f0f | ||
|
|
d44ceb6eb2 | ||
|
|
4fd0a8276b | ||
|
|
1d219a93ab | ||
|
|
f710cd6ecb | ||
|
|
571c1ac34c | ||
|
|
fb635987d1 | ||
|
|
a922751afc | ||
|
|
c1e3f56324 | ||
|
|
d0562e1ee6 | ||
|
|
4eb4c4b584 | ||
|
|
69a42e1fd2 | ||
|
|
fd498bebad | ||
|
|
23a8b5a66f | ||
|
|
e1e8d6124d | ||
|
|
10b2eae70c | ||
|
|
cfda9bb8ef | ||
|
|
d1edd386b6 | ||
|
|
b6a1020fc5 | ||
|
|
91ad861b26 | ||
|
|
d54ae5dad8 | ||
|
|
c8079d4115 | ||
|
|
90778a4e78 | ||
|
|
63a97a54e1 | ||
|
|
a2e8e232f1 | ||
|
|
7efc3e3770 | ||
|
|
3d58c6e916 | ||
|
|
c038cfe69a | ||
|
|
0770f25e79 | ||
|
|
8a9830282a | ||
|
|
8894a0b711 | ||
|
|
57a3ff8dfc | ||
|
|
6cff13623f | ||
|
|
b312f226b4 | ||
|
|
b627fa720b | ||
|
|
9faf178bdc | ||
|
|
c98f134b40 | ||
|
|
5838b26d90 | ||
|
|
7221fc4d4b | ||
|
|
a417760887 | ||
|
|
04d2dd3b1c | ||
|
|
6b88d37b73 | ||
|
|
d407ec1975 | ||
|
|
6d6a0734b0 | ||
|
|
0157a769c3 | ||
|
|
73932dd1c3 | ||
|
|
bc2abf3db4 | ||
|
|
073eb2c672 | ||
|
|
4744010295 | ||
|
|
d8d02cd6ec | ||
|
|
3e3299f549 | ||
|
|
eefc08087d | ||
|
|
fb6d199d36 | ||
|
|
fb3bf783dd | ||
|
|
fc268d43d0 | ||
|
|
c38aff7e52 | ||
|
|
7d4600f8c2 | ||
|
|
ff50ba4002 | ||
|
|
5c401b8e34 | ||
|
|
c96ebf15fd | ||
|
|
3e4239daf8 | ||
|
|
ab97c0126f | ||
|
|
d34d694731 | ||
|
|
e545897d4e | ||
|
|
caa6ec0e8a | ||
|
|
afbd9e2180 | ||
|
|
64116cd0d3 |
@@ -20,7 +20,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
cmake git zip libgpiod-dev libbluetooth-dev libi2c-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/* && \
|
||||
pip install --no-cache-dir -U \
|
||||
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
|
||||
/sdkconfig.*
|
||||
.dummy/*
|
||||
|
||||
# PYTHONPATH used by the Nix shell
|
||||
.python3
|
||||
|
||||
+9
-9
@@ -8,25 +8,25 @@ plugins:
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.497
|
||||
- renovate@42.81.8
|
||||
- prettier@3.8.0
|
||||
- trufflehog@3.92.4
|
||||
- checkov@3.2.501
|
||||
- renovate@43.10.3
|
||||
- prettier@3.8.1
|
||||
- trufflehog@3.93.3
|
||||
- yamllint@1.38.0
|
||||
- bandit@1.9.2
|
||||
- trivy@0.68.2
|
||||
- bandit@1.9.3
|
||||
- trivy@0.69.1
|
||||
- taplo@0.10.0
|
||||
- ruff@0.14.11
|
||||
- ruff@0.15.1
|
||||
- isort@7.0.0
|
||||
- markdownlint@0.47.0
|
||||
- oxipng@10.0.0
|
||||
- oxipng@10.1.0
|
||||
- svgo@4.0.0
|
||||
- actionlint@1.7.10
|
||||
- flake8@7.3.0
|
||||
- hadolint@2.14.0
|
||||
- shfmt@3.6.0
|
||||
- shellcheck@0.11.0
|
||||
- black@25.12.0
|
||||
- black@26.1.0
|
||||
- git-diff-check
|
||||
- gitleaks@8.30.0
|
||||
- clang-format@16.0.3
|
||||
|
||||
+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 \
|
||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
||||
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev \
|
||||
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
&& pip install --no-cache-dir -U platformio \
|
||||
&& mkdir /tmp/firmware
|
||||
@@ -53,7 +53,7 @@ USER root
|
||||
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 \
|
||||
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/* \
|
||||
&& mkdir -p /var/lib/meshtasticd \
|
||||
&& 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 \
|
||||
libgpiod-dev yaml-cpp-dev bluez-dev \
|
||||
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/* \
|
||||
&& pip install --no-cache-dir -U platformio \
|
||||
&& mkdir /tmp/firmware
|
||||
@@ -42,7 +42,7 @@ USER root
|
||||
|
||||
RUN apk --no-cache add \
|
||||
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/* \
|
||||
&& mkdir -p /var/lib/meshtasticd \
|
||||
&& mkdir -p /etc/meshtasticd/config.d \
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
Lora:
|
||||
Module: sx1262
|
||||
CS: 0
|
||||
IRQ: 6
|
||||
Reset: 1
|
||||
Busy: 4
|
||||
RXen: 2
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: ch341
|
||||
USB_PID: 0x5512
|
||||
USB_VID: 0x1A86
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# USB_Serialnum: 12345678
|
||||
SX126X_MAX_POWER: 22
|
||||
# Reduce output power to improve EMI
|
||||
NUM_PA_POINTS: 22
|
||||
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
|
||||
# Note: This module integrates an additional PA to achieve higher output power.
|
||||
# The 'power' parameter here does not represent the actual RF output.
|
||||
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
|
||||
# Each array element corresponds to the additional gain when that input level is set,
|
||||
# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index].
|
||||
# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information.
|
||||
@@ -1,15 +0,0 @@
|
||||
Lora:
|
||||
Module: sx1262
|
||||
CS: 0
|
||||
IRQ: 6
|
||||
Reset: 1
|
||||
Busy: 4
|
||||
RXen: 2
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: ch341
|
||||
USB_PID: 0x5512
|
||||
USB_VID: 0x1A86
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# USB_Serialnum: 12345678
|
||||
SX126X_MAX_POWER: 30
|
||||
# Reduce output power to improve EMI
|
||||
@@ -0,0 +1,23 @@
|
||||
Lora:
|
||||
Module: sx1268
|
||||
CS: 0
|
||||
IRQ: 6
|
||||
Reset: 1
|
||||
Busy: 4
|
||||
RXen: 2
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: ch341
|
||||
USB_PID: 0x5512
|
||||
USB_VID: 0x1A86
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# USB_Serialnum: 12345678
|
||||
SX126X_MAX_POWER: 22
|
||||
# Reduce output power to improve EMI
|
||||
NUM_PA_POINTS: 22
|
||||
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
|
||||
# Note: This module integrates an additional PA to achieve higher output power.
|
||||
# The 'power' parameter here does not represent the actual RF output.
|
||||
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
|
||||
# Each array element corresponds to the additional gain when that input level is set,
|
||||
# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index].
|
||||
# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information.
|
||||
@@ -1,15 +0,0 @@
|
||||
Lora:
|
||||
Module: sx1268
|
||||
CS: 0
|
||||
IRQ: 6
|
||||
Reset: 1
|
||||
Busy: 4
|
||||
RXen: 2
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: ch341
|
||||
USB_PID: 0x5512
|
||||
USB_VID: 0x1A86
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# USB_Serialnum: 12345678
|
||||
SX126X_MAX_POWER: 30
|
||||
# Reduce output power to improve EMI
|
||||
+2
-10
@@ -156,16 +156,8 @@ IF %BPS_RESET% EQU 1 (
|
||||
SET "PROGNAME=!FILENAME:.factory.bin=!"
|
||||
CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
|
||||
|
||||
IF "__!MCU!__" == "__esp32s3__" (
|
||||
@REM We are working with ESP32-S3
|
||||
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"
|
||||
)
|
||||
@REM Determine OTA filename based on MCU type (unified OTA format)
|
||||
SET "OTA_FILENAME=mt-!MCU!-ota.bin"
|
||||
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
|
||||
|
||||
@REM Set SPIFFS filename with "littlefs-" prefix.
|
||||
|
||||
+21
-14
@@ -32,6 +32,19 @@ if ! command -v jq >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# esptool v5 supports commands with dashes and deprecates commands with
|
||||
# underscores. Prior versions only support commands with underscores
|
||||
if ${ESPTOOL_CMD} | grep --quiet write-flash
|
||||
then
|
||||
ESPTOOL_WRITE_FLASH=write-flash
|
||||
ESPTOOL_ERASE_FLASH=erase-flash
|
||||
ESPTOOL_READ_FLASH_STATUS=read-flash-status
|
||||
else
|
||||
ESPTOOL_WRITE_FLASH=write_flash
|
||||
ESPTOOL_ERASE_FLASH=erase_flash
|
||||
ESPTOOL_READ_FLASH_STATUS=read_flash_status
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
# Usage info
|
||||
@@ -83,8 +96,8 @@ while [ $# -gt 0 ]; do
|
||||
done
|
||||
|
||||
if [[ $BPS_RESET == true ]]; then
|
||||
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status
|
||||
exit 0
|
||||
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset ${ESPTOOL_READ_FLASH_STATUS}
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[ -z "$FILENAME" ] && [ -n "$1" ] && {
|
||||
@@ -118,14 +131,8 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine OTA filename based on MCU type
|
||||
if [ "$MCU" == "esp32s3" ]; then
|
||||
OTAFILE=bleota-s3.bin
|
||||
elif [ "$MCU" == "esp32c3" ]; then
|
||||
OTAFILE=bleota-c3.bin
|
||||
else
|
||||
OTAFILE=bleota.bin
|
||||
fi
|
||||
# Determine OTA filename based on MCU type (unified OTA format)
|
||||
OTAFILE="mt-${MCU}-ota.bin"
|
||||
|
||||
# Set SPIFFS filename with "littlefs-" prefix.
|
||||
SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"
|
||||
@@ -144,12 +151,12 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
|
||||
fi
|
||||
|
||||
echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
|
||||
$ESPTOOL_CMD erase-flash
|
||||
$ESPTOOL_CMD write-flash $FIRMWARE_OFFSET "${FILENAME}"
|
||||
$ESPTOOL_CMD ${ESPTOOL_ERASE_FLASH}
|
||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $FIRMWARE_OFFSET "${FILENAME}"
|
||||
echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}"
|
||||
$ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}"
|
||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OTA_OFFSET "${OTAFILE}"
|
||||
echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}"
|
||||
$ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}"
|
||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OFFSET "${SPIFFSFILE}"
|
||||
|
||||
else
|
||||
show_help
|
||||
|
||||
+13
-2
@@ -20,6 +20,17 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# esptool v5 supports commands with dashes and deprecates commands with
|
||||
# underscores. Prior versions only support commands with underscores
|
||||
if ${ESPTOOL_CMD} | grep --quiet write-flash
|
||||
then
|
||||
ESPTOOL_WRITE_FLASH=write-flash
|
||||
ESPTOOL_READ_FLASH_STATUS=read-flash-status
|
||||
else
|
||||
ESPTOOL_WRITE_FLASH=write_flash
|
||||
ESPTOOL_READ_FLASH_STATUS=read_flash_status
|
||||
fi
|
||||
|
||||
# Usage info
|
||||
show_help() {
|
||||
cat << EOF
|
||||
@@ -69,7 +80,7 @@ done
|
||||
shift "$((OPTIND-1))"
|
||||
|
||||
if [ "$CHANGE_MODE" = true ]; then
|
||||
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status
|
||||
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset ${ESPTOOL_READ_FLASH_STATUS}
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -80,7 +91,7 @@ fi
|
||||
|
||||
if [[ -f "$FILENAME" && "$FILENAME" != *.factory.bin ]]; then
|
||||
echo "Trying to flash update ${FILENAME}"
|
||||
$ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}"
|
||||
$ESPTOOL_CMD --baud $FLASH_BAUD ${ESPTOOL_WRITE_FLASH} $UPDATE_OFFSET "${FILENAME}"
|
||||
else
|
||||
show_help
|
||||
echo "Invalid file: ${FILENAME}"
|
||||
|
||||
@@ -87,6 +87,12 @@
|
||||
</screenshots>
|
||||
|
||||
<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">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.19</url>
|
||||
</release>
|
||||
<release version="2.7.18" date="2026-01-02">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.18</url>
|
||||
</release>
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd"
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Minesemi ME25LS01",
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd"
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "MeshLink",
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v6.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DMINIMESH_LITE -DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [
|
||||
["0x239A", "0x8029"],
|
||||
["0x239A", "0x0029"],
|
||||
["0x239A", "0x002A"]
|
||||
],
|
||||
"usb_product": "Minimesh Lite",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "dls_Minimesh_Lite",
|
||||
"bsp": {
|
||||
"name": "adafruit"
|
||||
},
|
||||
"softdevice": {
|
||||
"sd_flags": "-DS140",
|
||||
"sd_name": "s140",
|
||||
"sd_version": "6.1.1",
|
||||
"sd_fwid": "0x00B6"
|
||||
},
|
||||
"bootloader": {
|
||||
"settings_addr": "0xFF000"
|
||||
}
|
||||
},
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Minimesh Lite",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_size": 815104,
|
||||
"speed": 115200,
|
||||
"protocol": "nrfutil",
|
||||
"protocols": ["nrfutil", "jlink", "nrfjprog", "stlink"],
|
||||
"use_1200bps_touch": true,
|
||||
"require_upload_port": true,
|
||||
"wait_for_upload_port": true
|
||||
},
|
||||
"url": "https://deeplabstudio.com",
|
||||
"vendor": "Deeplab Studio"
|
||||
}
|
||||
+2
-1
@@ -32,7 +32,8 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd"
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "MINEWSEMI_MS24SF1_SX1262",
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd"
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "BQ nRF52840",
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd"
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "ProMicro compatible nRF52840",
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd"
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Seeed T1000-E",
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd"
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino", "freertos"],
|
||||
"name": "Seeed WIO WM1110",
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd"
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Seeed WIO WM1110",
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd"
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Seeed WIO WM1110",
|
||||
|
||||
Vendored
+12
@@ -1,3 +1,15 @@
|
||||
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
|
||||
|
||||
* Version 2.7.19
|
||||
|
||||
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Thu, 22 Jan 2026 22:17:40 +0000
|
||||
|
||||
meshtasticd (2.7.18.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.18
|
||||
|
||||
Vendored
+2
-1
@@ -26,7 +26,8 @@ Build-Depends: debhelper-compat (= 13),
|
||||
libx11-dev,
|
||||
libinput-dev,
|
||||
libxkbcommon-x11-dev,
|
||||
libsqlite3-dev
|
||||
libsqlite3-dev,
|
||||
libsdl2-dev
|
||||
Standards-Version: 4.6.2
|
||||
Homepage: https://github.com/meshtastic/firmware
|
||||
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(libinput)
|
||||
BuildRequires: pkgconfig(xkbcommon-x11)
|
||||
BuildRequires: pkgconfig(sdl2)
|
||||
|
||||
# libbsd is needed on older Fedora/RHEL to provide 'strlcpy'
|
||||
%if 0%{?fedora} >= 39 || 0%{?rhel} >= 10
|
||||
@@ -59,8 +60,14 @@ BuildRequires: pkgconfig(libbsd-overlay)
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
%prep
|
||||
@@ -151,6 +158,7 @@ fi
|
||||
%license LICENSE
|
||||
%doc README.md
|
||||
%{_bindir}/meshtasticd
|
||||
%{_bindir}/meshtasticd-start.sh
|
||||
%dir %{_localstatedir}/lib/meshtasticd
|
||||
%{_udevrulesdir}/99-meshtasticd-udev.rules
|
||||
%dir %{_sysconfdir}/meshtasticd
|
||||
|
||||
@@ -43,13 +43,11 @@ class Esp32C3ExceptionDecoder(DeviceMonitorFilterBase):
|
||||
self.enabled = self.setup_paths()
|
||||
|
||||
if self.config.get("env:" + self.environment, "build_type") != "debug":
|
||||
print(
|
||||
"""
|
||||
print("""
|
||||
Please build project in debug configuration to get more details about an exception.
|
||||
See https://docs.platformio.org/page/projectconf/build_configurations.html
|
||||
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
return self
|
||||
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
});
|
||||
+39
-4
@@ -50,12 +50,15 @@ build_flags = -Wno-missing-field-initializers
|
||||
-DRADIOLIB_EXCLUDE_APRS=1
|
||||
-DRADIOLIB_EXCLUDE_LORAWAN=1
|
||||
-DMESHTASTIC_EXCLUDE_DROPZONE=1
|
||||
-DMESHTASTIC_EXCLUDE_REPLYBOT=1
|
||||
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
|
||||
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
||||
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
||||
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
||||
-DMESHTASTIC_EXCLUDE_POWERMON=1
|
||||
-DMESHTASTIC_EXCLUDE_STATUS=1
|
||||
-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
|
||||
#-D OLED_PL=1
|
||||
#-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs
|
||||
@@ -65,7 +68,7 @@ monitor_speed = 115200
|
||||
monitor_filters = direct
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/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
|
||||
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
|
||||
@@ -119,7 +122,7 @@ lib_deps =
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||
https://github.com/meshtastic/device-ui/archive/5a870c623a4e9ab7a7abe3d02950536f107d1a31.zip
|
||||
https://github.com/meshtastic/device-ui/archive/6c75195e9987b7a49563232234f2f868dd343cae.zip
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
[environmental_base]
|
||||
@@ -143,7 +146,7 @@ lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
|
||||
adafruit/Adafruit INA219@1.2.3
|
||||
# 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
|
||||
adafruit/Adafruit LIS3DH@1.3.0
|
||||
# renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0
|
||||
@@ -209,6 +212,38 @@ lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
|
||||
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
|
||||
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
|
||||
sensirion/Sensirion Core@0.7.2
|
||||
sensirion/Sensirion Core@0.7.3
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
||||
sensirion/Sensirion I2C SCD4x@1.1.0
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
|
||||
sensirion/Sensirion I2C SFA3x@1.0.0
|
||||
|
||||
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
|
||||
[environmental_extra_no_bsec]
|
||||
lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
|
||||
adafruit/Adafruit BMP3XX Library@2.1.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
|
||||
adafruit/Adafruit MAX1704X@1.0.3
|
||||
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
|
||||
adafruit/Adafruit SHTC3 Library@1.0.2
|
||||
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
|
||||
adafruit/Adafruit LPS2X@2.0.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library
|
||||
adafruit/Adafruit SHT31 Library@2.2.2
|
||||
# renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library
|
||||
adafruit/Adafruit VEML7700 Library@2.1.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library
|
||||
adafruit/Adafruit SHT4x Library@1.0.5
|
||||
# renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library
|
||||
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
|
||||
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
|
||||
closedcube/ClosedCube OPT3001@1.1.2
|
||||
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
|
||||
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
|
||||
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
|
||||
sensirion/Sensirion Core@0.7.3
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
||||
sensirion/Sensirion I2C SCD4x@1.1.0
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
|
||||
sensirion/Sensirion I2C SFA3x@1.0.0
|
||||
|
||||
+1
-1
Submodule protobufs updated: 4b9f104a18...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 "configuration.h"
|
||||
#include "detect/ScanI2C.h"
|
||||
#include "sleep.h"
|
||||
|
||||
#ifdef HAS_NCP5623
|
||||
#include <graphics/RAKled.h>
|
||||
NCP5623 rgb;
|
||||
#include <NCP5623.h>
|
||||
#endif
|
||||
|
||||
#ifdef HAS_LP5562
|
||||
#include <graphics/NomadStarLED.h>
|
||||
LP5562 rgbw;
|
||||
#endif
|
||||
|
||||
#ifdef HAS_NEOPIXEL
|
||||
#include <graphics/NeoPixel.h>
|
||||
Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE);
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
#endif
|
||||
|
||||
#ifdef UNPHONE
|
||||
@@ -21,10 +23,24 @@ Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE);
|
||||
extern unPhone unphone;
|
||||
#endif
|
||||
|
||||
namespace concurrency
|
||||
{
|
||||
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:
|
||||
explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting")
|
||||
{
|
||||
@@ -36,14 +52,15 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
moduleConfig.ambient_lighting.led_state = true;
|
||||
#endif
|
||||
#endif
|
||||
// Uncomment to test module
|
||||
// moduleConfig.ambient_lighting.led_state = true;
|
||||
// moduleConfig.ambient_lighting.current = 10;
|
||||
#if AMBIENT_LIGHTING_TEST
|
||||
// define to enable test
|
||||
moduleConfig.ambient_lighting.led_state = true;
|
||||
moduleConfig.ambient_lighting.current = 10;
|
||||
// Default to a color based on our node number
|
||||
// moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
|
||||
// moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
|
||||
// moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
|
||||
|
||||
moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
|
||||
moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
|
||||
moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
|
||||
#endif
|
||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||
_type = type;
|
||||
if (_type == ScanI2C::DeviceType::NONE) {
|
||||
@@ -53,11 +70,6 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
}
|
||||
#endif
|
||||
#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");
|
||||
#ifdef HAS_NCP5623
|
||||
if (_type == ScanI2C::NCP5623) {
|
||||
@@ -77,7 +89,13 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
pixels.clear(); // Set all pixel colors to 'off'
|
||||
pixels.setBrightness(moduleConfig.ambient_lighting.current);
|
||||
#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
|
||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||
}
|
||||
@@ -91,7 +109,8 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||
if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) {
|
||||
#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
|
||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||
}
|
||||
@@ -148,65 +167,53 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
return 0;
|
||||
}
|
||||
|
||||
void setLighting()
|
||||
protected:
|
||||
void setLighting(float current, uint8_t red, uint8_t green, uint8_t blue)
|
||||
{
|
||||
#ifdef HAS_NCP5623
|
||||
rgb.setCurrent(moduleConfig.ambient_lighting.current);
|
||||
rgb.setRed(moduleConfig.ambient_lighting.red);
|
||||
rgb.setGreen(moduleConfig.ambient_lighting.green);
|
||||
rgb.setBlue(moduleConfig.ambient_lighting.blue);
|
||||
LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d",
|
||||
moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
rgb.setCurrent(current);
|
||||
rgb.setRed(red);
|
||||
rgb.setGreen(green);
|
||||
rgb.setBlue(blue);
|
||||
LOG_DEBUG("Init NCP5623 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue);
|
||||
#endif
|
||||
#ifdef HAS_LP5562
|
||||
rgbw.setCurrent(moduleConfig.ambient_lighting.current);
|
||||
rgbw.setRed(moduleConfig.ambient_lighting.red);
|
||||
rgbw.setGreen(moduleConfig.ambient_lighting.green);
|
||||
rgbw.setBlue(moduleConfig.ambient_lighting.blue);
|
||||
LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current,
|
||||
moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
rgbw.setCurrent(current);
|
||||
rgbw.setRed(red);
|
||||
rgbw.setGreen(green);
|
||||
rgbw.setBlue(blue);
|
||||
LOG_DEBUG("Init LP5562 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue);
|
||||
#endif
|
||||
#ifdef HAS_NEOPIXEL
|
||||
pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green,
|
||||
moduleConfig.ambient_lighting.blue),
|
||||
0, NEOPIXEL_COUNT);
|
||||
pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT);
|
||||
|
||||
// RadioMaster Bandit has addressable LED at the two buttons
|
||||
// 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)
|
||||
pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1);
|
||||
#endif
|
||||
#if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX)
|
||||
pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1);
|
||||
#endif
|
||||
#endif
|
||||
pixels.show();
|
||||
// LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d",
|
||||
// moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
||||
// moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
// LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%f, red=%d, green=%d, blue=%d",
|
||||
// current, red, green, blue);
|
||||
#endif
|
||||
#ifdef RGBLED_CA
|
||||
analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red);
|
||||
analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green);
|
||||
analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue);
|
||||
LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
|
||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
analogWrite(RGBLED_RED, 255 - red);
|
||||
analogWrite(RGBLED_GREEN, 255 - green);
|
||||
analogWrite(RGBLED_BLUE, 255 - blue);
|
||||
LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", red, green, blue);
|
||||
#elif defined(RGBLED_RED)
|
||||
analogWrite(RGBLED_RED, moduleConfig.ambient_lighting.red);
|
||||
analogWrite(RGBLED_GREEN, moduleConfig.ambient_lighting.green);
|
||||
analogWrite(RGBLED_BLUE, moduleConfig.ambient_lighting.blue);
|
||||
LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
|
||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
analogWrite(RGBLED_RED, red);
|
||||
analogWrite(RGBLED_GREEN, green);
|
||||
analogWrite(RGBLED_BLUE, blue);
|
||||
LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", red, green, blue);
|
||||
#endif
|
||||
#ifdef UNPHONE
|
||||
unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green,
|
||||
moduleConfig.ambient_lighting.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);
|
||||
unphone.rgb(red, green, blue);
|
||||
LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", red, green, blue);
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace concurrency
|
||||
#endif // AMBIENTLIGHTINGTHREAD_H
|
||||
+2
-10
@@ -89,22 +89,14 @@ class BluetoothStatus : public Status
|
||||
case ConnectionState::CONNECTED:
|
||||
LOG_DEBUG("BluetoothStatus CONNECTED");
|
||||
#ifdef BLE_LED
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#else
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#endif
|
||||
digitalWrite(BLE_LED, LED_STATE_ON);
|
||||
#endif
|
||||
break;
|
||||
|
||||
case ConnectionState::DISCONNECTED:
|
||||
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
||||
#ifdef BLE_LED
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#else
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#endif
|
||||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
#include "Led.h"
|
||||
#include "PowerMon.h"
|
||||
#include "main.h"
|
||||
#include "power.h"
|
||||
|
||||
GpioVirtPin ledForceOn, ledBlink;
|
||||
|
||||
#if defined(LED_PIN)
|
||||
// Most boards have a GPIO for LED control
|
||||
static GpioHwPin ledRawHwPin(LED_PIN);
|
||||
#else
|
||||
static GpioVirtPin ledRawHwPin; // Dummy pin for no hardware
|
||||
#endif
|
||||
|
||||
#if LED_STATE_ON == 0
|
||||
static GpioVirtPin ledHwPin;
|
||||
static GpioNotTransformer ledInverter(&ledHwPin, &ledRawHwPin);
|
||||
#else
|
||||
static GpioPin &ledHwPin = ledRawHwPin;
|
||||
#endif
|
||||
|
||||
#if defined(HAS_PMU)
|
||||
/**
|
||||
* A GPIO controlled by the PMU
|
||||
*/
|
||||
class GpioPmuPin : public GpioPin
|
||||
{
|
||||
public:
|
||||
void set(bool value)
|
||||
{
|
||||
if (pmu_found && PMU) {
|
||||
// blink the axp led
|
||||
PMU->setChargingLedMode(value ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
|
||||
}
|
||||
}
|
||||
} ledPmuHwPin;
|
||||
|
||||
// In some cases we need to drive a PMU LED and a normal LED
|
||||
static GpioSplitter ledFinalPin(&ledHwPin, &ledPmuHwPin);
|
||||
#else
|
||||
static GpioPin &ledFinalPin = ledHwPin;
|
||||
#endif
|
||||
|
||||
#ifdef USE_POWERMON
|
||||
/**
|
||||
* We monitor changes to the LED drive output because we use that as a sanity test in our power monitor stuff.
|
||||
*/
|
||||
class MonitoredLedPin : public GpioPin
|
||||
{
|
||||
public:
|
||||
void set(bool value)
|
||||
{
|
||||
if (powerMon) {
|
||||
if (value)
|
||||
powerMon->setState(meshtastic_PowerMon_State_LED_On);
|
||||
else
|
||||
powerMon->clearState(meshtastic_PowerMon_State_LED_On);
|
||||
}
|
||||
ledFinalPin.set(value);
|
||||
}
|
||||
} monitoredLedPin;
|
||||
#else
|
||||
static GpioPin &monitoredLedPin = ledFinalPin;
|
||||
#endif
|
||||
|
||||
static GpioBinaryTransformer ledForcer(&ledForceOn, &ledBlink, &monitoredLedPin, GpioBinaryTransformer::Or);
|
||||
@@ -1,7 +0,0 @@
|
||||
#include "GpioLogic.h"
|
||||
#include "configuration.h"
|
||||
|
||||
/**
|
||||
* ledForceOn and ledForceOff both override the normal ledBlinker behavior (which is controlled by main)
|
||||
*/
|
||||
extern GpioVirtPin ledForceOn, ledBlink;
|
||||
+212
-96
@@ -1,11 +1,14 @@
|
||||
/**
|
||||
* @file Power.cpp
|
||||
* @brief This file contains the implementation of the Power class, which is responsible for managing power-related functionality
|
||||
* of the device. It includes battery level sensing, power management unit (PMU) control, and power state machine management. The
|
||||
* Power class is used by the main device class to manage power-related functionality.
|
||||
* @brief This file contains the implementation of the Power class, which is
|
||||
* responsible for managing power-related functionality of the device. It
|
||||
* includes battery level sensing, power management unit (PMU) control, and
|
||||
* power state machine management. The Power class is used by the main device
|
||||
* class to manage power-related functionality.
|
||||
*
|
||||
* The file also includes implementations of various battery level sensors, such as the AnalogBatteryLevel class, which assumes
|
||||
* the battery voltage is attached via a voltage-divider to an analog input.
|
||||
* The file also includes implementations of various battery level sensors, such
|
||||
* as the AnalogBatteryLevel class, which assumes the battery voltage is
|
||||
* attached via a voltage-divider to an analog input.
|
||||
*
|
||||
* This file is part of the Meshtastic project.
|
||||
* For more information, see: https://meshtastic.org/
|
||||
@@ -19,6 +22,7 @@
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "meshUtils.h"
|
||||
#include "power/PowerHAL.h"
|
||||
#include "sleep.h"
|
||||
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
@@ -171,22 +175,12 @@ Power *power;
|
||||
|
||||
using namespace meshtastic;
|
||||
|
||||
#ifndef AREF_VOLTAGE
|
||||
#if defined(ARCH_NRF52)
|
||||
/*
|
||||
* Internal Reference is +/-0.6V, with an adjustable gain of 1/6, 1/5, 1/4,
|
||||
* 1/3, 1/2 or 1, meaning 3.6, 3.0, 2.4, 1.8, 1.2 or 0.6V for the ADC levels.
|
||||
*
|
||||
* External Reference is VDD/4, with an adjustable gain of 1, 2 or 4, meaning
|
||||
* VDD/4, VDD/2 or VDD for the ADC levels.
|
||||
*
|
||||
* Default settings are internal reference with 1/6 gain (GND..3.6V ADC range)
|
||||
*/
|
||||
#define AREF_VOLTAGE 3.6
|
||||
#else
|
||||
// NRF52 has AREF_VOLTAGE defined in architecture.h but
|
||||
// make sure it's included. If something is wrong with NRF52
|
||||
// definition - compilation will fail on missing definition
|
||||
#if !defined(AREF_VOLTAGE) && !defined(ARCH_NRF52)
|
||||
#define AREF_VOLTAGE 3.3
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/**
|
||||
* If this board has a battery level sensor, set this to a valid implementation
|
||||
@@ -233,7 +227,8 @@ static void battery_adcDisable()
|
||||
#endif
|
||||
|
||||
/**
|
||||
* A simple battery level sensor that assumes the battery voltage is attached via a voltage-divider to an analog input
|
||||
* A simple battery level sensor that assumes the battery voltage is attached
|
||||
* via a voltage-divider to an analog input
|
||||
*/
|
||||
class AnalogBatteryLevel : public HasBatteryLevel
|
||||
{
|
||||
@@ -311,7 +306,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
|
||||
#ifndef BATTERY_SENSE_SAMPLES
|
||||
#define BATTERY_SENSE_SAMPLES \
|
||||
15 // Set the number of samples, it has an effect of increasing sensitivity in complex electromagnetic environment.
|
||||
15 // Set the number of samples, it has an effect of increasing sensitivity in
|
||||
// complex electromagnetic environment.
|
||||
#endif
|
||||
|
||||
#ifdef BATTERY_PIN
|
||||
@@ -341,7 +337,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
battery_adcDisable();
|
||||
|
||||
if (!initial_read_done) {
|
||||
// Flush the smoothing filter with an ADC reading, if the reading is plausibly correct
|
||||
// Flush the smoothing filter with an ADC reading, if the reading is
|
||||
// plausibly correct
|
||||
if (scaled > last_read_value)
|
||||
last_read_value = scaled;
|
||||
initial_read_done = true;
|
||||
@@ -350,8 +347,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF
|
||||
}
|
||||
|
||||
// LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u", BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t)
|
||||
// (last_read_value));
|
||||
// LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u",
|
||||
// BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t) (last_read_value));
|
||||
}
|
||||
return last_read_value;
|
||||
#endif // BATTERY_PIN
|
||||
@@ -420,7 +417,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
/**
|
||||
* return true if there is a battery installed in this unit
|
||||
*/
|
||||
// if we have a integrated device with a battery, we can assume that the battery is always connected
|
||||
// if we have a integrated device with a battery, we can assume that the
|
||||
// battery is always connected
|
||||
#ifdef BATTERY_IMMUTABLE
|
||||
virtual bool isBatteryConnect() override { return true; }
|
||||
#elif defined(ADC_V)
|
||||
@@ -441,10 +439,10 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
|
||||
#endif
|
||||
|
||||
/// If we see a battery voltage higher than physics allows - assume charger is pumping
|
||||
/// in power
|
||||
/// On some boards we don't have the power management chip (like AXPxxxx)
|
||||
/// so we use EXT_PWR_DETECT GPIO pin to detect external power source
|
||||
/// If we see a battery voltage higher than physics allows - assume charger is
|
||||
/// pumping in power On some boards we don't have the power management chip
|
||||
/// (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to detect external power
|
||||
/// source
|
||||
virtual bool isVbusIn() override
|
||||
{
|
||||
#ifdef EXT_PWR_DETECT
|
||||
@@ -461,8 +459,14 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
}
|
||||
// if it's not HIGH - check the battery
|
||||
#endif
|
||||
#elif defined(MUZI_BASE)
|
||||
return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk;
|
||||
// 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
|
||||
// but needs testing across multiple devices. NRF52 USB would not even work if
|
||||
// VBUS was not properly connected and detected by the CPU
|
||||
#elif defined(MUZI_BASE) || defined(PROMICRO_DIY_TCXO)
|
||||
return powerHAL_isVBUSConnected();
|
||||
#endif
|
||||
return getBattVoltage() > chargingVolt;
|
||||
}
|
||||
@@ -485,8 +489,9 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
#else
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION)
|
||||
if (hasINA()) {
|
||||
// get current flow from INA sensor - negative value means power flowing into the battery
|
||||
// default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT RESISTOR <--> INA_VIN- <--> LOAD
|
||||
// get current flow from INA sensor - negative value means power flowing
|
||||
// into the battery default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT
|
||||
// RESISTOR <--> INA_VIN- <--> LOAD
|
||||
LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address);
|
||||
#if defined(INA_CHARGING_DETECTION_INVERT)
|
||||
return getINACurrent() > 0;
|
||||
@@ -502,8 +507,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
}
|
||||
|
||||
private:
|
||||
/// If we see a battery voltage higher than physics allows - assume charger is pumping
|
||||
/// in power
|
||||
/// If we see a battery voltage higher than physics allows - assume charger is
|
||||
/// pumping in power
|
||||
|
||||
/// For heltecs with no battery connected, the measured voltage is 2204, so
|
||||
// need to be higher than that, in this case is 2500mV (3000-500)
|
||||
@@ -512,7 +517,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS;
|
||||
// Start value from minimum voltage for the filter to not start from 0
|
||||
// that could trigger some events.
|
||||
// This value is over-written by the first ADC reading, it the voltage seems reasonable.
|
||||
// This value is over-written by the first ADC reading, it the voltage seems
|
||||
// reasonable.
|
||||
bool initial_read_done = false;
|
||||
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
|
||||
uint32_t last_read_time_ms = 0;
|
||||
@@ -654,7 +660,8 @@ bool Power::analogInit()
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32S3
|
||||
// ESP32S3
|
||||
else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) {
|
||||
LOG_INFO("ADC config based on Two Point values and fitting curve coefficients stored in eFuse");
|
||||
LOG_INFO("ADC config based on Two Point values and fitting curve "
|
||||
"coefficients stored in eFuse");
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
@@ -662,13 +669,7 @@ bool Power::analogInit()
|
||||
}
|
||||
#endif // ARCH_ESP32
|
||||
|
||||
#ifdef ARCH_NRF52
|
||||
#ifdef VBAT_AR_INTERNAL
|
||||
analogReference(VBAT_AR_INTERNAL);
|
||||
#else
|
||||
analogReference(AR_INTERNAL); // 3.6V
|
||||
#endif
|
||||
#endif // ARCH_NRF52
|
||||
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
|
||||
|
||||
#ifndef ARCH_ESP32
|
||||
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||||
@@ -691,7 +692,9 @@ bool Power::setup()
|
||||
bool found = false;
|
||||
if (axpChipInit()) {
|
||||
found = true;
|
||||
} else if (lipoInit()) {
|
||||
} else if (cw2015Init()) {
|
||||
found = true;
|
||||
} else if (max17048Init()) {
|
||||
found = true;
|
||||
} else if (lipoChargerInit()) {
|
||||
found = true;
|
||||
@@ -723,6 +726,16 @@ bool Power::setup()
|
||||
runASAP = true;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
attachInterrupt(
|
||||
EXT_CHRG_DETECT,
|
||||
[]() {
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
enabled = found;
|
||||
low_voltage_counter = 0;
|
||||
@@ -769,7 +782,8 @@ void Power::reboot()
|
||||
HAL_NVIC_SystemReset();
|
||||
#else
|
||||
rebootAtMsec = -1;
|
||||
LOG_WARN("FIXME implement reboot for this platform. Note that some settings require a restart to be applied");
|
||||
LOG_WARN("FIXME implement reboot for this platform. Note that some settings "
|
||||
"require a restart to be applied");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -779,9 +793,12 @@ void Power::shutdown()
|
||||
#if HAS_SCREEN
|
||||
if (screen) {
|
||||
#ifdef T_DECK_PRO
|
||||
screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button
|
||||
screen->showSimpleBanner("Device is powered off.\nConnect USB to start!",
|
||||
0); // T-Deck Pro has no power button
|
||||
#elif defined(USE_EINK)
|
||||
screen->showSimpleBanner("Shutting Down...", 2250); // dismiss after 3 seconds to avoid the banner on the sleep screen
|
||||
screen->showSimpleBanner("Shutting Down...",
|
||||
2250); // dismiss after 3 seconds to avoid the
|
||||
// banner on the sleep screen
|
||||
#else
|
||||
screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
|
||||
#endif
|
||||
@@ -803,6 +820,9 @@ void Power::shutdown()
|
||||
#endif
|
||||
#ifdef PIN_LED3
|
||||
ledOff(PIN_LED3);
|
||||
#endif
|
||||
#ifdef LED_NOTIFICATION
|
||||
ledOff(LED_NOTIFICATION);
|
||||
#endif
|
||||
doDeepSleep(DELAY_FOREVER, true, true);
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
@@ -820,7 +840,8 @@ void Power::readPowerStatus()
|
||||
int32_t batteryVoltageMv = -1; // Assume unknown
|
||||
int8_t batteryChargePercent = -1;
|
||||
OptionalBool usbPowered = OptUnknown;
|
||||
OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM code doesn't run every time
|
||||
OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM
|
||||
// code doesn't run every time
|
||||
OptionalBool isChargingNow = OptUnknown;
|
||||
|
||||
if (batteryLevel) {
|
||||
@@ -833,9 +854,10 @@ void Power::readPowerStatus()
|
||||
if (batteryLevel->getBatteryPercent() >= 0) {
|
||||
batteryChargePercent = batteryLevel->getBatteryPercent();
|
||||
} else {
|
||||
// If the AXP192 returns a percentage less than 0, the feature is either not supported or there is an error
|
||||
// In that case, we compute an estimate of the charge percent based on open circuit voltage table defined
|
||||
// in power.h
|
||||
// If the AXP192 returns a percentage less than 0, the feature is either
|
||||
// not supported or there is an error In that case, we compute an
|
||||
// estimate of the charge percent based on open circuit voltage table
|
||||
// defined in power.h
|
||||
batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) /
|
||||
((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))),
|
||||
0, 100);
|
||||
@@ -843,12 +865,12 @@ void Power::readPowerStatus()
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way better instead to make a Nrf52IsUsbPowered subclass
|
||||
// (which shares a superclass with the BatteryLevel stuff)
|
||||
// that just provides a few methods. But in the interest of fixing this bug I'm going to follow current
|
||||
// practice.
|
||||
#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates the power states. Takes 20 seconds or so to detect
|
||||
// changes.
|
||||
// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way
|
||||
// better instead to make a Nrf52IsUsbPowered subclass (which shares a
|
||||
// superclass with the BatteryLevel stuff) that just provides a few methods. But
|
||||
// in the interest of fixing this bug I'm going to follow current practice.
|
||||
#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates
|
||||
// the power states. Takes 20 seconds or so to detect changes.
|
||||
|
||||
nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get();
|
||||
// LOG_DEBUG("NRF Power %d", nrf_usb_state);
|
||||
@@ -922,8 +944,9 @@ void Power::readPowerStatus()
|
||||
|
||||
#endif
|
||||
|
||||
// If we have a battery at all and it is less than 0%, force deep sleep if we have more than 10 low readings in
|
||||
// a row. NOTE: min LiIon/LiPo voltage is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough.
|
||||
// If we have a battery at all and it is less than 0%, force deep sleep if we
|
||||
// have more than 10 low readings in a row. NOTE: min LiIon/LiPo voltage
|
||||
// is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough.
|
||||
//
|
||||
|
||||
if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) {
|
||||
@@ -945,8 +968,8 @@ int32_t Power::runOnce()
|
||||
readPowerStatus();
|
||||
|
||||
#ifdef HAS_PMU
|
||||
// WE no longer use the IRQ line to wake the CPU (due to false wakes from sleep), but we do poll
|
||||
// the IRQ status by reading the registers over I2C
|
||||
// WE no longer use the IRQ line to wake the CPU (due to false wakes from
|
||||
// sleep), but we do poll the IRQ status by reading the registers over I2C
|
||||
if (PMU) {
|
||||
|
||||
PMU->getIrqStatus();
|
||||
@@ -988,7 +1011,8 @@ int32_t Power::runOnce()
|
||||
PMU->clearIrqStatus();
|
||||
}
|
||||
#endif
|
||||
// Only read once every 20 seconds once the power status for the app has been initialized
|
||||
// Only read once every 20 seconds once the power status for the app has been
|
||||
// initialized
|
||||
return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME;
|
||||
}
|
||||
|
||||
@@ -996,10 +1020,12 @@ int32_t Power::runOnce()
|
||||
* Init the power manager chip
|
||||
*
|
||||
* axp192 power
|
||||
DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose comms to the axp192 because the OLED and the
|
||||
axp192 share the same i2c bus, instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> ESP32 (keep this
|
||||
on!) LDO1 30mA -> charges GPS backup battery // charges the tiny J13 battery by the GPS to power the GPS ram (for a couple of
|
||||
days), can not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS
|
||||
DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose
|
||||
comms to the axp192 because the OLED and the axp192 share the same i2c bus,
|
||||
instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max ->
|
||||
ESP32 (keep this on!) LDO1 30mA -> charges GPS backup battery // charges the
|
||||
tiny J13 battery by the GPS to power the GPS ram (for a couple of days), can
|
||||
not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS
|
||||
*
|
||||
*/
|
||||
bool Power::axpChipInit()
|
||||
@@ -1044,9 +1070,10 @@ bool Power::axpChipInit()
|
||||
|
||||
if (!PMU) {
|
||||
/*
|
||||
* In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will be called at the same time.
|
||||
* In order not to affect other devices, if the initialization of the PMU fails, Wire needs to be re-initialized once,
|
||||
* if there are multiple devices sharing the bus.
|
||||
* In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will
|
||||
* be called at the same time. In order not to affect other devices, if the
|
||||
* initialization of the PMU fails, Wire needs to be re-initialized once, if
|
||||
* there are multiple devices sharing the bus.
|
||||
* * */
|
||||
#ifndef PMU_USE_WIRE1
|
||||
w->begin(I2C_SDA, I2C_SCL);
|
||||
@@ -1063,8 +1090,8 @@ bool Power::axpChipInit()
|
||||
PMU->enablePowerOutput(XPOWERS_LDO2);
|
||||
|
||||
// oled module power channel,
|
||||
// disable it will cause abnormal communication between boot and AXP power supply,
|
||||
// do not turn it off
|
||||
// disable it will cause abnormal communication between boot and AXP power
|
||||
// supply, do not turn it off
|
||||
PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300);
|
||||
// enable oled power
|
||||
PMU->enablePowerOutput(XPOWERS_DCDC1);
|
||||
@@ -1091,7 +1118,8 @@ bool Power::axpChipInit()
|
||||
PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2);
|
||||
} else if (PMU->getChipModel() == XPOWERS_AXP2101) {
|
||||
|
||||
/*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it uses an AXP2101 power chip*/
|
||||
/*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it
|
||||
* uses an AXP2101 power chip*/
|
||||
if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) {
|
||||
// Unuse power channel
|
||||
PMU->disablePowerOutput(XPOWERS_DCDC2);
|
||||
@@ -1126,8 +1154,8 @@ bool Power::axpChipInit()
|
||||
// t-beam s3 core
|
||||
/**
|
||||
* gnss module power channel
|
||||
* The default ALDO4 is off, you need to turn on the GNSS power first, otherwise it will be invalid during
|
||||
* initialization
|
||||
* The default ALDO4 is off, you need to turn on the GNSS power first,
|
||||
* otherwise it will be invalid during initialization
|
||||
*/
|
||||
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
|
||||
PMU->enablePowerOutput(XPOWERS_ALDO4);
|
||||
@@ -1177,7 +1205,8 @@ bool Power::axpChipInit()
|
||||
// disable all axp chip interrupt
|
||||
PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
|
||||
|
||||
// Set the constant current charging current of AXP2101, temporarily use 500mA by default
|
||||
// Set the constant current charging current of AXP2101, temporarily use
|
||||
// 500mA by default
|
||||
PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA);
|
||||
|
||||
// Set up the charging voltage
|
||||
@@ -1243,11 +1272,12 @@ bool Power::axpChipInit()
|
||||
PMU->getPowerChannelVoltage(XPOWERS_BLDO2));
|
||||
}
|
||||
|
||||
// We can safely ignore this approach for most (or all) boards because MCU turned off
|
||||
// earlier than battery discharged to 2.6V.
|
||||
// We can safely ignore this approach for most (or all) boards because MCU
|
||||
// turned off earlier than battery discharged to 2.6V.
|
||||
//
|
||||
// Unfortanly for now we can't use this killswitch for RAK4630-based boards because they have a bug with
|
||||
// battery voltage measurement. Probably it sometimes drops to low values.
|
||||
// Unfortunately for now we can't use this killswitch for RAK4630-based boards
|
||||
// because they have a bug with battery voltage measurement. Probably it
|
||||
// sometimes drops to low values.
|
||||
#ifndef RAK4630
|
||||
// Set PMU shutdown voltage at 2.6V to maximize battery utilization
|
||||
PMU->setSysPowerDownVoltage(2600);
|
||||
@@ -1266,10 +1296,12 @@ bool Power::axpChipInit()
|
||||
attachInterrupt(
|
||||
PMU_IRQ, [] { pmu_irq = true; }, FALLING);
|
||||
|
||||
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ because it occurs repeatedly while there is
|
||||
// no battery also it could cause inadvertent waking from light sleep just because the battery filled
|
||||
// we don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while no battery installed
|
||||
// we don't look at AXPXXX_VBUS_REMOVED_IRQ because we don't have anything hooked to vbus
|
||||
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ
|
||||
// because it occurs repeatedly while there is no battery also it could cause
|
||||
// inadvertent waking from light sleep just because the battery filled we
|
||||
// don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while
|
||||
// no battery installed we don't look at AXPXXX_VBUS_REMOVED_IRQ because we
|
||||
// don't have anything hooked to vbus
|
||||
PMU->enableIRQ(pmuIrqMask);
|
||||
|
||||
PMU->clearIrqStatus();
|
||||
@@ -1291,7 +1323,7 @@ bool Power::axpChipInit()
|
||||
/**
|
||||
* Wrapper class for an I2C MAX17048 Lipo battery sensor.
|
||||
*/
|
||||
class LipoBatteryLevel : public HasBatteryLevel
|
||||
class MAX17048BatteryLevel : public HasBatteryLevel
|
||||
{
|
||||
private:
|
||||
MAX17048Singleton *max17048 = nullptr;
|
||||
@@ -1339,18 +1371,18 @@ class LipoBatteryLevel : public HasBatteryLevel
|
||||
virtual bool isCharging() override { return max17048->isBatteryCharging(); }
|
||||
};
|
||||
|
||||
LipoBatteryLevel lipoLevel;
|
||||
MAX17048BatteryLevel max17048Level;
|
||||
|
||||
/**
|
||||
* Init the Lipo battery level sensor
|
||||
*/
|
||||
bool Power::lipoInit()
|
||||
bool Power::max17048Init()
|
||||
{
|
||||
bool result = lipoLevel.runOnce();
|
||||
LOG_DEBUG("Power::lipoInit lipo sensor is %s", result ? "ready" : "not ready yet");
|
||||
bool result = max17048Level.runOnce();
|
||||
LOG_DEBUG("Power::max17048Init lipo sensor is %s", result ? "ready" : "not ready yet");
|
||||
if (!result)
|
||||
return false;
|
||||
batteryLevel = &lipoLevel;
|
||||
batteryLevel = &max17048Level;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1358,7 +1390,88 @@ bool Power::lipoInit()
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -1385,8 +1498,8 @@ class LipoCharger : public HasBatteryLevel
|
||||
bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
|
||||
if (result) {
|
||||
LOG_INFO("PPM BQ25896 init succeeded");
|
||||
// Set the minimum operating voltage. Below this voltage, the PPM will protect
|
||||
// PPM->setSysPowerDownVoltage(3100);
|
||||
// Set the minimum operating voltage. Below this voltage, the PPM will
|
||||
// protect PPM->setSysPowerDownVoltage(3100);
|
||||
|
||||
// Set input current limit, default is 500mA
|
||||
// PPM->setInputCurrentLimit(800);
|
||||
@@ -1409,7 +1522,8 @@ class LipoCharger : public HasBatteryLevel
|
||||
PPM->enableMeasure();
|
||||
|
||||
// Turn on charging function
|
||||
// If there is no battery connected, do not turn on the charging function
|
||||
// If there is no battery connected, do not turn on the charging
|
||||
// function
|
||||
PPM->enableCharge();
|
||||
} else {
|
||||
LOG_WARN("PPM BQ25896 init failed");
|
||||
@@ -1444,7 +1558,8 @@ class LipoCharger : public HasBatteryLevel
|
||||
virtual int getBatteryPercent() override
|
||||
{
|
||||
return -1;
|
||||
// return bq->getChargePercent(); // don't use BQ27220 for battery percent, it is not calibrated
|
||||
// return bq->getChargePercent(); // don't use BQ27220 for battery percent,
|
||||
// it is not calibrated
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1566,7 +1681,8 @@ bool Power::meshSolarInit()
|
||||
|
||||
#else
|
||||
/**
|
||||
* The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel
|
||||
* The meshSolar battery level sensor is unavailable - default to
|
||||
* AnalogBatteryLevel
|
||||
*/
|
||||
bool Power::meshSolarInit()
|
||||
{
|
||||
|
||||
+4
-4
@@ -9,13 +9,13 @@
|
||||
*/
|
||||
#include "PowerFSM.h"
|
||||
#include "Default.h"
|
||||
#include "Led.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerMon.h"
|
||||
#include "configuration.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "main.h"
|
||||
#include "modules/StatusLEDModule.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
|
||||
@@ -103,7 +103,7 @@ static void lsIdle()
|
||||
uint32_t sleepTime = SLEEP_TIME;
|
||||
|
||||
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);
|
||||
powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep);
|
||||
|
||||
@@ -111,7 +111,7 @@ static void lsIdle()
|
||||
case ESP_SLEEP_WAKEUP_TIMER:
|
||||
// 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
|
||||
|
||||
secsSlept += sleepTime;
|
||||
@@ -146,7 +146,7 @@ static void lsIdle()
|
||||
}
|
||||
} else {
|
||||
// Time to stop sleeping!
|
||||
ledBlink.set(false);
|
||||
statusLEDModule->setPowerLED(false);
|
||||
LOG_INFO("Reached ls_secs, service loop()");
|
||||
powerFSM.trigger(EVENT_WAKE_TIMER);
|
||||
}
|
||||
|
||||
+14
-18
@@ -155,6 +155,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#endif
|
||||
|
||||
// Default system gain to 0 if not defined
|
||||
#ifndef NUM_PA_POINTS
|
||||
#define NUM_PA_POINTS 1
|
||||
#endif
|
||||
|
||||
#ifndef TX_GAIN_LORA
|
||||
#define TX_GAIN_LORA 0
|
||||
#endif
|
||||
@@ -213,6 +217,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define SHTC3_ADDR 0x70
|
||||
#define LPS22HB_ADDR 0x5C
|
||||
#define LPS22HB_ADDR_ALT 0x5D
|
||||
#define SFA30_ADDR 0x5D
|
||||
#define SHT31_4x_ADDR 0x44
|
||||
#define SHT31_4x_ADDR_ALT 0x45
|
||||
#define PMSA003I_ADDR 0x12
|
||||
@@ -229,6 +234,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define NAU7802_ADDR 0x2A
|
||||
#define MAX30102_ADDR 0x57
|
||||
#define SCD4X_ADDR 0x62
|
||||
#define CW2015_ADDR 0x62
|
||||
#define MLX90614_ADDR_DEF 0x5A
|
||||
#define CGRADSENS_ADDR 0x66
|
||||
#define LTR390UV_ADDR 0x53
|
||||
@@ -237,6 +243,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define BQ27220_ADDR 0x55 // same address as TDECK_KB
|
||||
#define BQ25896_ADDR 0x6B
|
||||
#define LTR553ALS_ADDR 0x23
|
||||
#define SEN5X_ADDR 0x69
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ACCELEROMETER
|
||||
@@ -386,9 +393,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#ifndef HAS_RADIO
|
||||
#define HAS_RADIO 0
|
||||
#endif
|
||||
#ifndef HAS_RTC
|
||||
#define HAS_RTC 0
|
||||
#endif
|
||||
#ifndef HAS_CPU_SHUTDOWN
|
||||
#define HAS_CPU_SHUTDOWN 0
|
||||
#endif
|
||||
@@ -424,12 +428,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define HAS_RGB_LED
|
||||
#endif
|
||||
|
||||
#ifndef LED_STATE_OFF
|
||||
#define LED_STATE_OFF 0
|
||||
#endif
|
||||
#ifndef LED_STATE_ON
|
||||
#define LED_STATE_ON 1
|
||||
#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
|
||||
#if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN)
|
||||
@@ -445,18 +453,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// BME680 BSEC2 support detection
|
||||
#if !defined(MESHTASTIC_BME680_BSEC2_SUPPORTED)
|
||||
#if defined(RAK_4631) || defined(TBEAM_V10)
|
||||
|
||||
#define MESHTASTIC_BME680_BSEC2_SUPPORTED 1
|
||||
#define MESHTASTIC_BME680_HEADER <bsec2.h>
|
||||
#else
|
||||
#define MESHTASTIC_BME680_BSEC2_SUPPORTED 0
|
||||
#define MESHTASTIC_BME680_HEADER <Adafruit_BME680.h>
|
||||
#endif // defined(RAK_4631)
|
||||
#endif // !defined(MESHTASTIC_BME680_BSEC2_SUPPORTED)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Global switches to turn off features for a minimized build
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -43,8 +43,8 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstAQI() const
|
||||
{
|
||||
ScanI2C::DeviceType types[] = {PMSA003I, SCD4X};
|
||||
return firstOfOrNONE(2, types);
|
||||
ScanI2C::DeviceType types[] = {PMSA003I, SEN5X, SCD4X, SFA30};
|
||||
return firstOfOrNONE(4, types);
|
||||
}
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstRGBLED() const
|
||||
|
||||
@@ -88,7 +88,10 @@ class ScanI2C
|
||||
BH1750,
|
||||
DA217,
|
||||
CHSC6X,
|
||||
CST226SE
|
||||
CST226SE,
|
||||
SEN5X,
|
||||
SFA30,
|
||||
CW2015
|
||||
} DeviceType;
|
||||
|
||||
// typedef uint8_t DeviceAddress;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "ScanI2CTwoWire.h"
|
||||
#include "configuration.h"
|
||||
#include "detect/ScanI2C.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C
|
||||
|
||||
@@ -8,6 +10,7 @@
|
||||
#endif
|
||||
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
|
||||
#include "meshUtils.h" // vformat
|
||||
|
||||
#endif
|
||||
|
||||
bool in_array(uint8_t *array, int size, uint8_t lookfor)
|
||||
@@ -114,6 +117,45 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
|
||||
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, ...) \
|
||||
case ADDR: \
|
||||
logFoundDevice(__VA_ARGS__); \
|
||||
@@ -416,6 +458,13 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
break;
|
||||
|
||||
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(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address)
|
||||
|
||||
@@ -541,7 +590,17 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
break;
|
||||
|
||||
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);
|
||||
#ifdef HAS_TPS65233
|
||||
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;
|
||||
|
||||
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
|
||||
// ICM20948 Register check
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
|
||||
#ifdef HAS_ICM20948
|
||||
type = ICM20948;
|
||||
@@ -580,14 +640,31 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
type = ICM20948;
|
||||
logFoundDevice("ICM20948", (uint8_t)addr.address);
|
||||
break;
|
||||
} else if (addr.address == BMX160_ADDR) {
|
||||
type = BMX160;
|
||||
logFoundDevice("BMX160", (uint8_t)addr.address);
|
||||
break;
|
||||
} else {
|
||||
type = MPU6050;
|
||||
logFoundDevice("MPU6050", (uint8_t)addr.address);
|
||||
break;
|
||||
String prod = "";
|
||||
prod = readSEN5xProductName(i2cBus, addr.address);
|
||||
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;
|
||||
|
||||
|
||||
@@ -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 <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
|
||||
|
||||
@@ -905,6 +905,12 @@ void GPS::writePinStandby(bool standby)
|
||||
// Write and log
|
||||
pinMode(PIN_GPS_STANDBY, OUTPUT);
|
||||
digitalWrite(PIN_GPS_STANDBY, val);
|
||||
|
||||
// Enter backup mode on PA1010D; TODO: may be applicable to other MTK GPS too
|
||||
if (IS_ONE_OF(gnssModel, GNSS_MODEL_MTK_PA1010D)) {
|
||||
_serial_gps->write("$PMTK225,4*2F\r\n");
|
||||
}
|
||||
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW");
|
||||
#endif
|
||||
|
||||
+11
-10
@@ -72,11 +72,13 @@ RTCSetResult readFromRTC()
|
||||
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
|
||||
#if defined(PCF8563_RTC)
|
||||
if (rtc_found.address == PCF8563_RTC) {
|
||||
SensorPCF8563 rtc;
|
||||
#elif defined(PCF85063_RTC)
|
||||
if (rtc_found.address == PCF85063_RTC) {
|
||||
SensorPCF85063 rtc;
|
||||
|
||||
#endif
|
||||
uint32_t now = millis();
|
||||
SensorRtcHelper rtc;
|
||||
|
||||
#if WIRE_INTERFACES_COUNT == 2
|
||||
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)
|
||||
#if defined(PCF8563_RTC)
|
||||
if (rtc_found.address == PCF8563_RTC) {
|
||||
SensorPCF8563 rtc;
|
||||
#elif defined(PCF85063_RTC)
|
||||
if (rtc_found.address == PCF85063_RTC) {
|
||||
SensorPCF85063 rtc;
|
||||
|
||||
#endif
|
||||
SensorRtcHelper rtc;
|
||||
|
||||
#if WIRE_INTERFACES_COUNT == 2
|
||||
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);
|
||||
#endif
|
||||
|
||||
// nrf52 doesn't have a readable RTC (yet - software not written)
|
||||
#if HAS_RTC
|
||||
readFromRTC();
|
||||
#endif
|
||||
|
||||
return RTCSetResultSuccess;
|
||||
} else {
|
||||
return RTCSetResultNotSet; // RTC was already set with a higher quality time
|
||||
@@ -397,7 +397,7 @@ uint32_t getValidTime(RTCQuality minQuality, bool local)
|
||||
return (currentQuality >= minQuality) ? getTime(local) : 0;
|
||||
}
|
||||
|
||||
time_t gm_mktime(struct tm *tm)
|
||||
time_t gm_mktime(const struct tm *tm)
|
||||
{
|
||||
#if !MESHTASTIC_EXCLUDE_TZ
|
||||
time_t result = 0;
|
||||
@@ -413,8 +413,8 @@ time_t gm_mktime(struct tm *tm)
|
||||
days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400);
|
||||
|
||||
// Now, within this tm->year, compute the days *before* this tm->month starts.
|
||||
int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year
|
||||
int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11
|
||||
static const int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year
|
||||
int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11
|
||||
|
||||
// If this is a leap year, and we're past February, add a day:
|
||||
if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) {
|
||||
@@ -435,6 +435,7 @@ time_t gm_mktime(struct tm *tm)
|
||||
|
||||
return result;
|
||||
#else
|
||||
return mktime(tm);
|
||||
struct tm tmCopy = *tm;
|
||||
return mktime(&tmCopy);
|
||||
#endif
|
||||
}
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ uint32_t getValidTime(RTCQuality minQuality, bool local = false);
|
||||
|
||||
RTCSetResult readFromRTC();
|
||||
|
||||
time_t gm_mktime(struct tm *tm);
|
||||
time_t gm_mktime(const struct tm *tm);
|
||||
|
||||
#define SEC_PER_DAY 86400
|
||||
#define SEC_PER_HOUR 3600
|
||||
|
||||
@@ -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
|
||||
@@ -1731,6 +1731,26 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
showFrame(FrameDirection::PREVIOUS);
|
||||
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||
showFrame(FrameDirection::NEXT);
|
||||
} else if (event->inputEvent == INPUT_BROKER_FN_F1) {
|
||||
this->ui->switchToFrame(0);
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
} else if (event->inputEvent == INPUT_BROKER_FN_F2) {
|
||||
this->ui->switchToFrame(1);
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
} else if (event->inputEvent == INPUT_BROKER_FN_F3) {
|
||||
this->ui->switchToFrame(2);
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
} else if (event->inputEvent == INPUT_BROKER_FN_F4) {
|
||||
this->ui->switchToFrame(3);
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
} else if (event->inputEvent == INPUT_BROKER_FN_F5) {
|
||||
this->ui->switchToFrame(4);
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
} else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
// Long press up button for fast frame switching
|
||||
showPrevFrame();
|
||||
|
||||
@@ -221,7 +221,6 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
|
||||
if (rtc_sec > 0) {
|
||||
// === Build Time String ===
|
||||
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
int hour, minute, second;
|
||||
graphics::decomposeTime(rtc_sec, hour, minute, second);
|
||||
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)
|
||||
{
|
||||
bool drawConnectionState = false;
|
||||
if (service->api_state == service->STATE_BLE || service->api_state == service->STATE_WIFI ||
|
||||
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 (!isAPIConnected(service->api_state))
|
||||
return;
|
||||
|
||||
if (drawConnectionState) {
|
||||
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
|
||||
(connection_icon_height * scale) + (2 * scale));
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||||
int iconX = 0;
|
||||
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
||||
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
|
||||
(connection_icon_height * scale) + (2 * scale));
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||||
int iconX = 0;
|
||||
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
||||
|
||||
for (int yy = 0; yy < connection_icon_height; ++yy) {
|
||||
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
|
||||
for (int xx = 0; xx < connection_icon_width; ++xx) {
|
||||
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
||||
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
|
||||
if (byteVal & bitMask) {
|
||||
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
|
||||
}
|
||||
for (int yy = 0; yy < connection_icon_height; ++yy) {
|
||||
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
|
||||
for (int xx = 0; xx < connection_icon_width; ++xx) {
|
||||
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
||||
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
|
||||
if (byteVal & bitMask) {
|
||||
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);
|
||||
|
||||
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
|
||||
|
||||
@@ -110,14 +110,14 @@ void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, ui
|
||||
uint32_t secs = (uptimeMillis % 60000) / 1000;
|
||||
|
||||
if (days) {
|
||||
snprintf(uptimeStr, maxLength, "%s: %ud %uh", prefix, days, hours);
|
||||
snprintf(uptimeStr, maxLength, "%s%ud %uh", prefix, days, 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) {
|
||||
snprintf(uptimeStr, maxLength, "%s: %um", prefix, mins);
|
||||
snprintf(uptimeStr, maxLength, "%s%um", prefix, mins);
|
||||
} else if (mins) {
|
||||
snprintf(uptimeStr, maxLength, "%s: %um %us", prefix, mins, secs);
|
||||
snprintf(uptimeStr, maxLength, "%s%um %us", prefix, mins, secs);
|
||||
} 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';
|
||||
}
|
||||
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());
|
||||
@@ -518,9 +522,13 @@ char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)
|
||||
|
||||
char c = key.character;
|
||||
|
||||
// Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings
|
||||
if (isLongPress && c >= 'a' && c <= 'z') {
|
||||
c = (char)(c - 'a' + 'A');
|
||||
// Long-press: letters become uppercase; for "?" provide "/" like a typical keyboard
|
||||
if (isLongPress) {
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
c = (char)(c - 'a' + 'A');
|
||||
} else if (c == '?') {
|
||||
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
|
||||
char uptimeStr[32] = "";
|
||||
getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr));
|
||||
getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr));
|
||||
textWidth = display->getStringWidth(uptimeStr);
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
display->drawString(nameX, getTextPositions(display)[line++], uptimeStr);
|
||||
|
||||
+171
-178
@@ -58,7 +58,7 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp
|
||||
|
||||
} // namespace
|
||||
|
||||
menuHandler::screenMenus menuHandler::menuQueue = menu_none;
|
||||
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
|
||||
uint32_t menuHandler::pickedNodeNum = 0;
|
||||
bool test_enabled = false;
|
||||
uint8_t test_count = 0;
|
||||
@@ -66,7 +66,7 @@ uint8_t test_count = 0;
|
||||
void menuHandler::loraMenu()
|
||||
{
|
||||
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;
|
||||
bannerOptions.message = "LoRa Actions";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -74,14 +74,14 @@ void menuHandler::loraMenu()
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
// No action
|
||||
} else if (selected == device_role_picker) {
|
||||
menuHandler::menuQueue = menuHandler::device_role_picker;
|
||||
} else if (selected == radio_preset_picker) {
|
||||
menuHandler::menuQueue = menuHandler::radio_preset_picker;
|
||||
} else if (selected == frequency_slot) {
|
||||
menuHandler::menuQueue = menuHandler::frequency_slot;
|
||||
} else if (selected == lora_picker) {
|
||||
menuHandler::menuQueue = menuHandler::lora_picker;
|
||||
} else if (selected == DeviceRolePicker) {
|
||||
menuHandler::menuQueue = menuHandler::DeviceRolePicker;
|
||||
} else if (selected == RadioPresetPicker) {
|
||||
menuHandler::menuQueue = menuHandler::RadioPresetPicker;
|
||||
} else if (selected == FrequencySlot) {
|
||||
menuHandler::menuQueue = menuHandler::FrequencySlot;
|
||||
} else if (selected == LoraPicker) {
|
||||
menuHandler::menuQueue = menuHandler::LoraPicker;
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -102,7 +102,7 @@ void menuHandler::OnboardMessage()
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 2;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
menuHandler::menuQueue = menuHandler::no_timeout_lora_picker;
|
||||
menuHandler::menuQueue = menuHandler::NoTimeoutLoraPicker;
|
||||
screen->runNow();
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -216,7 +216,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::DeviceRolePicker()
|
||||
void menuHandler::deviceRolePicker()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
|
||||
enum optionsNumbers {
|
||||
@@ -232,7 +232,7 @@ void menuHandler::DeviceRolePicker()
|
||||
bannerOptions.optionsCount = 5;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::lora_Menu;
|
||||
menuHandler::menuQueue = menuHandler::LoraMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
} else if (selected == devicerole_client) {
|
||||
@@ -266,52 +266,8 @@ void menuHandler::FrequencySlotPicker()
|
||||
|
||||
// Calculate number of channels (copied from RadioInterface::applyModemConfig())
|
||||
meshtastic_Config_LoRaConfig &loraConfig = config.lora;
|
||||
double bw = loraConfig.bandwidth;
|
||||
if (loraConfig.use_preset) {
|
||||
switch (loraConfig.modem_preset) {
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
|
||||
bw = (myRegion->wideLora) ? 1625.0 : 500;
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
|
||||
bw = (myRegion->wideLora) ? 812.5 : 250;
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
|
||||
bw = (myRegion->wideLora) ? 812.5 : 250;
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
|
||||
bw = (myRegion->wideLora) ? 812.5 : 250;
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
|
||||
bw = (myRegion->wideLora) ? 812.5 : 250;
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
|
||||
bw = (myRegion->wideLora) ? 1625.0 : 500;
|
||||
break;
|
||||
default:
|
||||
bw = (myRegion->wideLora) ? 812.5 : 250;
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
|
||||
bw = (myRegion->wideLora) ? 406.25 : 125;
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
|
||||
bw = (myRegion->wideLora) ? 406.25 : 125;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
bw = loraConfig.bandwidth;
|
||||
if (bw == 31) // This parameter is not an integer
|
||||
bw = 31.25;
|
||||
if (bw == 62) // Fix for 62.5Khz bandwidth
|
||||
bw = 62.5;
|
||||
if (bw == 200)
|
||||
bw = 203.125;
|
||||
if (bw == 400)
|
||||
bw = 406.25;
|
||||
if (bw == 800)
|
||||
bw = 812.5;
|
||||
if (bw == 1600)
|
||||
bw = 1625.0;
|
||||
}
|
||||
double bw = loraConfig.use_preset ? modemPresetToBwKHz(loraConfig.modem_preset, myRegion->wideLora)
|
||||
: bwCodeToKHz(loraConfig.bandwidth);
|
||||
|
||||
uint32_t numChannels = 0;
|
||||
if (myRegion) {
|
||||
@@ -344,7 +300,7 @@ void menuHandler::FrequencySlotPicker()
|
||||
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::lora_Menu;
|
||||
menuHandler::menuQueue = menuHandler::LoraMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -357,7 +313,7 @@ void menuHandler::FrequencySlotPicker()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::RadioPresetPicker()
|
||||
void menuHandler::radioPresetPicker()
|
||||
{
|
||||
static const RadioPresetOption presetOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
@@ -377,7 +333,7 @@ void menuHandler::RadioPresetPicker()
|
||||
auto bannerOptions =
|
||||
createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuHandler::menuQueue = menuHandler::lora_Menu;
|
||||
menuHandler::menuQueue = menuHandler::LoraMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -396,7 +352,7 @@ void menuHandler::RadioPresetPicker()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::TwelveHourPicker()
|
||||
void menuHandler::twelveHourPicker()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
|
||||
enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 };
|
||||
@@ -406,7 +362,7 @@ void menuHandler::TwelveHourPicker()
|
||||
bannerOptions.optionsCount = 3;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::clock_menu;
|
||||
menuHandler::menuQueue = menuHandler::ClockMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == twelve) {
|
||||
config.display.use_12h_clock = true;
|
||||
@@ -434,7 +390,7 @@ void menuHandler::showConfirmationBanner(const char *message, std::function<void
|
||||
screen->showOverlayBanner(confirmBanner);
|
||||
}
|
||||
|
||||
void menuHandler::ClockFacePicker()
|
||||
void menuHandler::clockFacePicker()
|
||||
{
|
||||
static const ClockFaceOption clockFaceOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
@@ -448,7 +404,7 @@ void menuHandler::ClockFacePicker()
|
||||
auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels,
|
||||
[](const ClockFaceOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuHandler::menuQueue = menuHandler::clock_menu;
|
||||
menuHandler::menuQueue = menuHandler::ClockMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -500,7 +456,7 @@ void menuHandler::TZPicker()
|
||||
auto bannerOptions = createStaticBannerOptions(
|
||||
"Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuHandler::menuQueue = menuHandler::clock_menu;
|
||||
menuHandler::menuQueue = menuHandler::ClockMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -547,13 +503,13 @@ void menuHandler::clockMenu()
|
||||
bannerOptions.optionsCount = 4;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Clock) {
|
||||
menuHandler::menuQueue = menuHandler::clock_face_picker;
|
||||
menuHandler::menuQueue = menuHandler::ClockFacePicker;
|
||||
screen->runNow();
|
||||
} else if (selected == Time) {
|
||||
menuHandler::menuQueue = menuHandler::twelve_hour_picker;
|
||||
menuHandler::menuQueue = menuHandler::TwelveHourPicker;
|
||||
screen->runNow();
|
||||
} else if (selected == Timezone) {
|
||||
menuHandler::menuQueue = menuHandler::TZ_picker;
|
||||
menuHandler::menuQueue = menuHandler::TzPicker;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -616,12 +572,12 @@ void menuHandler::messageResponseMenu()
|
||||
LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer);
|
||||
|
||||
if (selected == ViewMode) {
|
||||
menuHandler::menuQueue = menuHandler::message_viewmode_menu;
|
||||
menuHandler::menuQueue = menuHandler::MessageViewModeMenu;
|
||||
screen->runNow();
|
||||
|
||||
// Reply submenu
|
||||
} else if (selected == ReplyMenu) {
|
||||
menuHandler::menuQueue = menuHandler::reply_menu;
|
||||
menuHandler::menuQueue = menuHandler::ReplyMenu;
|
||||
screen->runNow();
|
||||
|
||||
} else if (selected == MuteChannel) {
|
||||
@@ -633,7 +589,7 @@ void menuHandler::messageResponseMenu()
|
||||
}
|
||||
|
||||
} else if (selected == DeleteMenu) {
|
||||
menuHandler::menuQueue = menuHandler::delete_messages_menu;
|
||||
menuHandler::menuQueue = menuHandler::DeleteMessagesMenu;
|
||||
screen->runNow();
|
||||
|
||||
#ifdef HAS_I2S
|
||||
@@ -693,7 +649,7 @@ void menuHandler::replyMenu()
|
||||
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::message_response_menu;
|
||||
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -781,7 +737,7 @@ void menuHandler::deleteMessagesMenu()
|
||||
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::message_response_menu;
|
||||
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -945,7 +901,7 @@ void menuHandler::messageViewModeMenu()
|
||||
bannerOptions.bannerCallback = [=](int selected) -> void {
|
||||
LOG_DEBUG("messageViewModeMenu: selected=%d", selected);
|
||||
if (selected == -1) {
|
||||
menuHandler::menuQueue = menuHandler::message_response_menu;
|
||||
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == -2) {
|
||||
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL);
|
||||
@@ -1127,23 +1083,23 @@ void menuHandler::systemBaseMenu()
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Notifications) {
|
||||
menuHandler::menuQueue = menuHandler::buzzermodemenupicker;
|
||||
menuHandler::menuQueue = menuHandler::BuzzerModeMenuPicker;
|
||||
screen->runNow();
|
||||
} else if (selected == ScreenOptions) {
|
||||
menuHandler::menuQueue = menuHandler::screen_options_menu;
|
||||
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == PowerMenu) {
|
||||
menuHandler::menuQueue = menuHandler::power_menu;
|
||||
menuHandler::menuQueue = menuHandler::PowerMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == Test) {
|
||||
menuHandler::menuQueue = menuHandler::test_menu;
|
||||
menuHandler::menuQueue = menuHandler::TestMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == Bluetooth) {
|
||||
menuQueue = bluetooth_toggle_menu;
|
||||
menuQueue = BluetoothToggleMenu;
|
||||
screen->runNow();
|
||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||
} else if (selected == WiFiToggle) {
|
||||
menuQueue = wifi_toggle_menu;
|
||||
menuQueue = WifiToggleMenu;
|
||||
screen->runNow();
|
||||
#endif
|
||||
} else if (selected == Back && !test_enabled) {
|
||||
@@ -1221,7 +1177,7 @@ void menuHandler::favoriteBaseMenu()
|
||||
evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
|
||||
screen->handleUIFrameEvent(&evt);
|
||||
} else if (selected == Remove) {
|
||||
menuHandler::menuQueue = menuHandler::remove_favorite;
|
||||
menuHandler::menuQueue = menuHandler::RemoveFavorite;
|
||||
screen->runNow();
|
||||
} else if (selected == TraceRoute) {
|
||||
if (traceRouteModule) {
|
||||
@@ -1282,15 +1238,15 @@ void menuHandler::positionBaseMenu()
|
||||
auto action = static_cast<PositionAction>(option.value);
|
||||
switch (action) {
|
||||
case PositionAction::GpsToggle:
|
||||
menuQueue = gps_toggle_menu;
|
||||
menuQueue = GpsToggleMenu;
|
||||
screen->runNow();
|
||||
break;
|
||||
case PositionAction::GpsFormat:
|
||||
menuQueue = gps_format_menu;
|
||||
menuQueue = GpsFormatMenu;
|
||||
screen->runNow();
|
||||
break;
|
||||
case PositionAction::CompassMenu:
|
||||
menuQueue = compass_point_north_menu;
|
||||
menuQueue = CompassPointNorthMenu;
|
||||
screen->runNow();
|
||||
break;
|
||||
case PositionAction::CompassCalibrate:
|
||||
@@ -1299,15 +1255,15 @@ void menuHandler::positionBaseMenu()
|
||||
}
|
||||
break;
|
||||
case PositionAction::GPSSmartPosition:
|
||||
menuQueue = gps_smart_position_menu;
|
||||
menuQueue = GpsSmartPositionMenu;
|
||||
screen->runNow();
|
||||
break;
|
||||
case PositionAction::GPSUpdateInterval:
|
||||
menuQueue = gps_update_interval_menu;
|
||||
menuQueue = GpsUpdateIntervalMenu;
|
||||
screen->runNow();
|
||||
break;
|
||||
case PositionAction::GPSPositionBroadcast:
|
||||
menuQueue = gps_position_broadcast_menu;
|
||||
menuQueue = GpsPositionBroadcastMenu;
|
||||
screen->runNow();
|
||||
break;
|
||||
}
|
||||
@@ -1347,13 +1303,13 @@ void menuHandler::nodeListMenu()
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == NodePicker) {
|
||||
menuQueue = NodePicker_menu;
|
||||
menuQueue = NodePickerMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == Reset) {
|
||||
menuQueue = reset_node_db_menu;
|
||||
menuQueue = ResetNodeDbMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == NodeNameLength) {
|
||||
menuHandler::menuQueue = menuHandler::node_name_length_menu;
|
||||
menuHandler::menuQueue = menuHandler::NodeNameLengthMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -1374,12 +1330,12 @@ void menuHandler::NodePicker()
|
||||
menuHandler::pickedNodeNum = nodenum;
|
||||
// Keep UI favorite context in sync (used elsewhere for some node-based actions)
|
||||
graphics::UIRenderer::currentFavoriteNodeNum = nodenum;
|
||||
menuQueue = Manage_Node_menu;
|
||||
menuQueue = ManageNodeMenu;
|
||||
screen->runNow();
|
||||
});
|
||||
}
|
||||
|
||||
void menuHandler::ManageNodeMenu()
|
||||
void menuHandler::manageNodeMenu()
|
||||
{
|
||||
// If we don't have a node selected yet, go fast exit
|
||||
auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
|
||||
@@ -1435,7 +1391,7 @@ void menuHandler::ManageNodeMenu()
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
menuQueue = node_base_menu;
|
||||
menuQueue = NodeBaseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -1527,7 +1483,7 @@ void menuHandler::nodeNameLengthMenu()
|
||||
auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels,
|
||||
[](const NodeNameOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuQueue = node_base_menu;
|
||||
menuQueue = NodeBaseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -1542,6 +1498,7 @@ void menuHandler::nodeNameLengthMenu()
|
||||
|
||||
config.display.use_long_node_name = option.value;
|
||||
saveUIConfig();
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
LOG_INFO("Setting names to %s", option.value ? "long" : "short");
|
||||
});
|
||||
|
||||
@@ -1572,7 +1529,7 @@ void menuHandler::resetNodeDBMenu()
|
||||
nodeDB->resetNodes(1);
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
} else if (selected == 0) {
|
||||
menuQueue = node_base_menu;
|
||||
menuQueue = NodeBaseMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -1594,7 +1551,7 @@ void menuHandler::compassNorthMenu()
|
||||
auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels,
|
||||
[](const CompassOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuQueue = position_base_menu;
|
||||
menuQueue = PositionBaseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -1639,7 +1596,7 @@ void menuHandler::GPSToggleMenu()
|
||||
auto bannerOptions =
|
||||
createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuQueue = position_base_menu;
|
||||
menuQueue = PositionBaseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -1704,7 +1661,7 @@ void menuHandler::GPSFormatMenu()
|
||||
|
||||
auto onSelection = [](const GPSFormatOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuQueue = position_base_menu;
|
||||
menuQueue = PositionBaseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -1759,7 +1716,7 @@ void menuHandler::GPSSmartPositionMenu()
|
||||
bannerOptions.optionsCount = 3;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == 0) {
|
||||
menuQueue = position_base_menu;
|
||||
menuQueue = PositionBaseMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == 1) {
|
||||
config.position.position_broadcast_smart_enabled = true;
|
||||
@@ -1788,7 +1745,7 @@ void menuHandler::GPSUpdateIntervalMenu()
|
||||
bannerOptions.optionsCount = 16;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == 0) {
|
||||
menuQueue = position_base_menu;
|
||||
menuQueue = PositionBaseMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == 1) {
|
||||
config.position.gps_update_interval = 8;
|
||||
@@ -1876,7 +1833,7 @@ void menuHandler::GPSPositionBroadcastMenu()
|
||||
bannerOptions.optionsCount = 17;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == 0) {
|
||||
menuQueue = position_base_menu;
|
||||
menuQueue = PositionBaseMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == 1) {
|
||||
config.position.position_broadcast_secs = 60;
|
||||
@@ -1959,7 +1916,7 @@ void menuHandler::GPSPositionBroadcastMenu()
|
||||
|
||||
#endif
|
||||
|
||||
void menuHandler::BluetoothToggleMenu()
|
||||
void menuHandler::bluetoothToggleMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
@@ -2087,7 +2044,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
|
||||
auto bannerOptions = createStaticBannerOptions(
|
||||
"Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuQueue = system_base_menu;
|
||||
menuQueue = SystemBaseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -2182,7 +2139,7 @@ void menuHandler::rebootMenu()
|
||||
messageStore.saveToFlash();
|
||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||
} else {
|
||||
menuQueue = power_menu;
|
||||
menuQueue = PowerMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -2204,7 +2161,7 @@ void menuHandler::shutdownMenu()
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
} else {
|
||||
menuQueue = power_menu;
|
||||
menuQueue = PowerMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -2265,14 +2222,14 @@ void menuHandler::testMenu()
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == NumberPicker) {
|
||||
menuQueue = number_test;
|
||||
menuQueue = NumberTest;
|
||||
screen->runNow();
|
||||
} else if (selected == ShowChirpy) {
|
||||
screen->toggleFrameVisibility("chirpy");
|
||||
screen->setFrames(Screen::FOCUS_SYSTEM);
|
||||
|
||||
} else {
|
||||
menuQueue = system_base_menu;
|
||||
menuQueue = SystemBaseMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -2296,7 +2253,7 @@ void menuHandler::wifiBaseMenu()
|
||||
bannerOptions.optionsCount = 2;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Wifi_toggle) {
|
||||
menuQueue = wifi_toggle_menu;
|
||||
menuQueue = WifiToggleMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -2345,9 +2302,9 @@ void menuHandler::screenOptionsMenu()
|
||||
hasSupportBrightness = false;
|
||||
#endif
|
||||
|
||||
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits };
|
||||
static const char *optionsArray[5] = {"Back"};
|
||||
static int optionsEnumArray[5] = {Back};
|
||||
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles };
|
||||
static const char *optionsArray[6] = {"Back"};
|
||||
static int optionsEnumArray[6] = {Back};
|
||||
int options = 1;
|
||||
|
||||
// Only show brightness for B&W displays
|
||||
@@ -2369,6 +2326,9 @@ void menuHandler::screenOptionsMenu()
|
||||
optionsArray[options] = "Display Units";
|
||||
optionsEnumArray[options++] = DisplayUnits;
|
||||
|
||||
optionsArray[options] = "Message Bubbles";
|
||||
optionsEnumArray[options++] = MessageBubbles;
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Display Options";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -2376,10 +2336,10 @@ void menuHandler::screenOptionsMenu()
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Brightness) {
|
||||
menuHandler::menuQueue = menuHandler::brightness_picker;
|
||||
menuHandler::menuQueue = menuHandler::BrightnessPicker;
|
||||
screen->runNow();
|
||||
} else if (selected == ScreenColor) {
|
||||
menuHandler::menuQueue = menuHandler::tftcolormenupicker;
|
||||
menuHandler::menuQueue = menuHandler::TftColorMenuPicker;
|
||||
screen->runNow();
|
||||
} else if (selected == FrameToggles) {
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
@@ -2387,8 +2347,11 @@ void menuHandler::screenOptionsMenu()
|
||||
} else if (selected == DisplayUnits) {
|
||||
menuHandler::menuQueue = menuHandler::DisplayUnits;
|
||||
screen->runNow();
|
||||
} else if (selected == MessageBubbles) {
|
||||
menuHandler::menuQueue = menuHandler::MessageBubblesMenu;
|
||||
screen->runNow();
|
||||
} else {
|
||||
menuQueue = system_base_menu;
|
||||
menuQueue = SystemBaseMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -2424,16 +2387,16 @@ void menuHandler::powerMenu()
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Reboot) {
|
||||
menuHandler::menuQueue = menuHandler::reboot_menu;
|
||||
menuHandler::menuQueue = menuHandler::RebootMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == Shutdown) {
|
||||
menuHandler::menuQueue = menuHandler::shutdown_menu;
|
||||
menuHandler::menuQueue = menuHandler::ShutdownMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == MUI) {
|
||||
menuHandler::menuQueue = menuHandler::mui_picker;
|
||||
menuHandler::menuQueue = menuHandler::MuiPicker;
|
||||
screen->runNow();
|
||||
} else {
|
||||
menuQueue = system_base_menu;
|
||||
menuQueue = SystemBaseMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -2471,7 +2434,7 @@ void menuHandler::keyVerificationFinalPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
void menuHandler::FrameToggles_menu()
|
||||
void menuHandler::frameTogglesMenu()
|
||||
{
|
||||
enum optionsNumbers {
|
||||
Finish,
|
||||
@@ -2615,7 +2578,7 @@ void menuHandler::FrameToggles_menu()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::DisplayUnits_menu()
|
||||
void menuHandler::displayUnitsMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, MetricUnits, ImperialUnits };
|
||||
|
||||
@@ -2636,7 +2599,34 @@ void menuHandler::DisplayUnits_menu()
|
||||
config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL;
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
} 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();
|
||||
}
|
||||
};
|
||||
@@ -2645,153 +2635,156 @@ void menuHandler::DisplayUnits_menu()
|
||||
|
||||
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
{
|
||||
if (menuQueue != menu_none)
|
||||
if (menuQueue != MenuNone)
|
||||
test_count = 0;
|
||||
switch (menuQueue) {
|
||||
case menu_none:
|
||||
case MenuNone:
|
||||
break;
|
||||
case lora_Menu:
|
||||
case LoraMenu:
|
||||
loraMenu();
|
||||
break;
|
||||
case lora_picker:
|
||||
case LoraPicker:
|
||||
LoraRegionPicker();
|
||||
break;
|
||||
case device_role_picker:
|
||||
DeviceRolePicker();
|
||||
case DeviceRolePicker:
|
||||
deviceRolePicker();
|
||||
break;
|
||||
case radio_preset_picker:
|
||||
RadioPresetPicker();
|
||||
case RadioPresetPicker:
|
||||
radioPresetPicker();
|
||||
break;
|
||||
case frequency_slot:
|
||||
case FrequencySlot:
|
||||
FrequencySlotPicker();
|
||||
break;
|
||||
case no_timeout_lora_picker:
|
||||
case NoTimeoutLoraPicker:
|
||||
LoraRegionPicker(0);
|
||||
break;
|
||||
case TZ_picker:
|
||||
case TzPicker:
|
||||
TZPicker();
|
||||
break;
|
||||
case twelve_hour_picker:
|
||||
TwelveHourPicker();
|
||||
case TwelveHourPicker:
|
||||
twelveHourPicker();
|
||||
break;
|
||||
case clock_face_picker:
|
||||
ClockFacePicker();
|
||||
case ClockFacePicker:
|
||||
clockFacePicker();
|
||||
break;
|
||||
case clock_menu:
|
||||
case ClockMenu:
|
||||
clockMenu();
|
||||
break;
|
||||
case system_base_menu:
|
||||
case SystemBaseMenu:
|
||||
systemBaseMenu();
|
||||
break;
|
||||
case position_base_menu:
|
||||
case PositionBaseMenu:
|
||||
positionBaseMenu();
|
||||
break;
|
||||
case node_base_menu:
|
||||
case NodeBaseMenu:
|
||||
nodeListMenu();
|
||||
break;
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
case gps_toggle_menu:
|
||||
case GpsToggleMenu:
|
||||
GPSToggleMenu();
|
||||
break;
|
||||
case gps_format_menu:
|
||||
case GpsFormatMenu:
|
||||
GPSFormatMenu();
|
||||
break;
|
||||
case gps_smart_position_menu:
|
||||
case GpsSmartPositionMenu:
|
||||
GPSSmartPositionMenu();
|
||||
break;
|
||||
case gps_update_interval_menu:
|
||||
case GpsUpdateIntervalMenu:
|
||||
GPSUpdateIntervalMenu();
|
||||
break;
|
||||
case gps_position_broadcast_menu:
|
||||
case GpsPositionBroadcastMenu:
|
||||
GPSPositionBroadcastMenu();
|
||||
break;
|
||||
#endif
|
||||
case compass_point_north_menu:
|
||||
case CompassPointNorthMenu:
|
||||
compassNorthMenu();
|
||||
break;
|
||||
case reset_node_db_menu:
|
||||
case ResetNodeDbMenu:
|
||||
resetNodeDBMenu();
|
||||
break;
|
||||
case buzzermodemenupicker:
|
||||
case BuzzerModeMenuPicker:
|
||||
BuzzerModeMenu();
|
||||
break;
|
||||
case mui_picker:
|
||||
case MuiPicker:
|
||||
switchToMUIMenu();
|
||||
break;
|
||||
case tftcolormenupicker:
|
||||
case TftColorMenuPicker:
|
||||
TFTColorPickerMenu(display);
|
||||
break;
|
||||
case brightness_picker:
|
||||
case BrightnessPicker:
|
||||
BrightnessPickerMenu();
|
||||
break;
|
||||
case node_name_length_menu:
|
||||
case NodeNameLengthMenu:
|
||||
nodeNameLengthMenu();
|
||||
break;
|
||||
case reboot_menu:
|
||||
case RebootMenu:
|
||||
rebootMenu();
|
||||
break;
|
||||
case shutdown_menu:
|
||||
case ShutdownMenu:
|
||||
shutdownMenu();
|
||||
break;
|
||||
case NodePicker_menu:
|
||||
case NodePickerMenu:
|
||||
NodePicker();
|
||||
break;
|
||||
case Manage_Node_menu:
|
||||
ManageNodeMenu();
|
||||
case ManageNodeMenu:
|
||||
manageNodeMenu();
|
||||
break;
|
||||
case remove_favorite:
|
||||
case RemoveFavorite:
|
||||
removeFavoriteMenu();
|
||||
break;
|
||||
case trace_route_menu:
|
||||
case TraceRouteMenu:
|
||||
traceRouteMenu();
|
||||
break;
|
||||
case test_menu:
|
||||
case TestMenu:
|
||||
testMenu();
|
||||
break;
|
||||
case number_test:
|
||||
case NumberTest:
|
||||
numberTest();
|
||||
break;
|
||||
case wifi_toggle_menu:
|
||||
case WifiToggleMenu:
|
||||
wifiToggleMenu();
|
||||
break;
|
||||
case key_verification_init:
|
||||
case KeyVerificationInit:
|
||||
keyVerificationInitMenu();
|
||||
break;
|
||||
case key_verification_final_prompt:
|
||||
case KeyVerificationFinalPrompt:
|
||||
keyVerificationFinalPrompt();
|
||||
break;
|
||||
case bluetooth_toggle_menu:
|
||||
BluetoothToggleMenu();
|
||||
case BluetoothToggleMenu:
|
||||
bluetoothToggleMenu();
|
||||
break;
|
||||
case screen_options_menu:
|
||||
case ScreenOptionsMenu:
|
||||
screenOptionsMenu();
|
||||
break;
|
||||
case power_menu:
|
||||
case PowerMenu:
|
||||
powerMenu();
|
||||
break;
|
||||
case FrameToggles:
|
||||
FrameToggles_menu();
|
||||
frameTogglesMenu();
|
||||
break;
|
||||
case DisplayUnits:
|
||||
DisplayUnits_menu();
|
||||
displayUnitsMenu();
|
||||
break;
|
||||
case throttle_message:
|
||||
case ThrottleMessage:
|
||||
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
|
||||
break;
|
||||
case message_response_menu:
|
||||
case MessageResponseMenu:
|
||||
messageResponseMenu();
|
||||
break;
|
||||
case reply_menu:
|
||||
case ReplyMenu:
|
||||
replyMenu();
|
||||
break;
|
||||
case delete_messages_menu:
|
||||
case DeleteMessagesMenu:
|
||||
deleteMessagesMenu();
|
||||
break;
|
||||
case message_viewmode_menu:
|
||||
case MessageViewModeMenu:
|
||||
messageViewModeMenu();
|
||||
break;
|
||||
case MessageBubblesMenu:
|
||||
messageBubblesMenu();
|
||||
break;
|
||||
}
|
||||
menuQueue = menu_none;
|
||||
menuQueue = MenuNone;
|
||||
}
|
||||
|
||||
void menuHandler::saveUIConfig()
|
||||
|
||||
@@ -8,53 +8,54 @@ class menuHandler
|
||||
{
|
||||
public:
|
||||
enum screenMenus {
|
||||
menu_none,
|
||||
lora_Menu,
|
||||
lora_picker,
|
||||
device_role_picker,
|
||||
radio_preset_picker,
|
||||
frequency_slot,
|
||||
no_timeout_lora_picker,
|
||||
TZ_picker,
|
||||
twelve_hour_picker,
|
||||
clock_face_picker,
|
||||
clock_menu,
|
||||
position_base_menu,
|
||||
node_base_menu,
|
||||
gps_toggle_menu,
|
||||
gps_format_menu,
|
||||
gps_smart_position_menu,
|
||||
gps_update_interval_menu,
|
||||
gps_position_broadcast_menu,
|
||||
compass_point_north_menu,
|
||||
reset_node_db_menu,
|
||||
buzzermodemenupicker,
|
||||
mui_picker,
|
||||
tftcolormenupicker,
|
||||
brightness_picker,
|
||||
reboot_menu,
|
||||
shutdown_menu,
|
||||
NodePicker_menu,
|
||||
Manage_Node_menu,
|
||||
remove_favorite,
|
||||
test_menu,
|
||||
number_test,
|
||||
wifi_toggle_menu,
|
||||
bluetooth_toggle_menu,
|
||||
screen_options_menu,
|
||||
power_menu,
|
||||
system_base_menu,
|
||||
key_verification_init,
|
||||
key_verification_final_prompt,
|
||||
trace_route_menu,
|
||||
throttle_message,
|
||||
message_response_menu,
|
||||
message_viewmode_menu,
|
||||
reply_menu,
|
||||
delete_messages_menu,
|
||||
node_name_length_menu,
|
||||
MenuNone,
|
||||
LoraMenu,
|
||||
LoraPicker,
|
||||
DeviceRolePicker,
|
||||
RadioPresetPicker,
|
||||
FrequencySlot,
|
||||
NoTimeoutLoraPicker,
|
||||
TzPicker,
|
||||
TwelveHourPicker,
|
||||
ClockFacePicker,
|
||||
ClockMenu,
|
||||
PositionBaseMenu,
|
||||
NodeBaseMenu,
|
||||
GpsToggleMenu,
|
||||
GpsFormatMenu,
|
||||
GpsSmartPositionMenu,
|
||||
GpsUpdateIntervalMenu,
|
||||
GpsPositionBroadcastMenu,
|
||||
CompassPointNorthMenu,
|
||||
ResetNodeDbMenu,
|
||||
BuzzerModeMenuPicker,
|
||||
MuiPicker,
|
||||
TftColorMenuPicker,
|
||||
BrightnessPicker,
|
||||
RebootMenu,
|
||||
ShutdownMenu,
|
||||
NodePickerMenu,
|
||||
ManageNodeMenu,
|
||||
RemoveFavorite,
|
||||
TestMenu,
|
||||
NumberTest,
|
||||
WifiToggleMenu,
|
||||
BluetoothToggleMenu,
|
||||
ScreenOptionsMenu,
|
||||
PowerMenu,
|
||||
SystemBaseMenu,
|
||||
KeyVerificationInit,
|
||||
KeyVerificationFinalPrompt,
|
||||
TraceRouteMenu,
|
||||
ThrottleMessage,
|
||||
MessageResponseMenu,
|
||||
MessageViewModeMenu,
|
||||
ReplyMenu,
|
||||
DeleteMessagesMenu,
|
||||
NodeNameLengthMenu,
|
||||
FrameToggles,
|
||||
DisplayUnits
|
||||
DisplayUnits,
|
||||
MessageBubblesMenu
|
||||
};
|
||||
static screenMenus menuQueue;
|
||||
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
|
||||
@@ -62,15 +63,15 @@ class menuHandler
|
||||
static void OnboardMessage();
|
||||
static void LoraRegionPicker(uint32_t duration = 30000);
|
||||
static void loraMenu();
|
||||
static void DeviceRolePicker();
|
||||
static void RadioPresetPicker();
|
||||
static void deviceRolePicker();
|
||||
static void radioPresetPicker();
|
||||
static void FrequencySlotPicker();
|
||||
static void handleMenuSwitch(OLEDDisplay *display);
|
||||
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
|
||||
static void clockMenu();
|
||||
static void TZPicker();
|
||||
static void TwelveHourPicker();
|
||||
static void ClockFacePicker();
|
||||
static void twelveHourPicker();
|
||||
static void clockFacePicker();
|
||||
static void messageResponseMenu();
|
||||
static void messageViewModeMenu();
|
||||
static void replyMenu();
|
||||
@@ -95,7 +96,7 @@ class menuHandler
|
||||
static void rebootMenu();
|
||||
static void shutdownMenu();
|
||||
static void NodePicker();
|
||||
static void ManageNodeMenu();
|
||||
static void manageNodeMenu();
|
||||
static void addFavoriteMenu();
|
||||
static void removeFavoriteMenu();
|
||||
static void traceRouteMenu();
|
||||
@@ -106,15 +107,16 @@ class menuHandler
|
||||
static void screenOptionsMenu();
|
||||
static void powerMenu();
|
||||
static void nodeNameLengthMenu();
|
||||
static void FrameToggles_menu();
|
||||
static void DisplayUnits_menu();
|
||||
static void frameTogglesMenu();
|
||||
static void displayUnitsMenu();
|
||||
static void messageBubblesMenu();
|
||||
static void textMessageMenu();
|
||||
|
||||
private:
|
||||
static void saveUIConfig();
|
||||
static void keyVerificationInitMenu();
|
||||
static void keyVerificationFinalPrompt();
|
||||
static void BluetoothToggleMenu();
|
||||
static void bluetoothToggleMenu();
|
||||
};
|
||||
|
||||
/* Generic Menu Options designations */
|
||||
@@ -140,7 +142,7 @@ struct ScreenColor {
|
||||
uint8_t b;
|
||||
bool useVariant;
|
||||
|
||||
ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
|
||||
explicit ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
|
||||
: r(rIn), g(gIn), b(bIn), useVariant(variantIn)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "MessageStore.h"
|
||||
#include "NodeDB.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
@@ -20,7 +19,6 @@
|
||||
|
||||
// External declarations
|
||||
extern bool hasUnreadMessage;
|
||||
extern meshtastic_DeviceState devicestate;
|
||||
extern graphics::Screen *screen;
|
||||
|
||||
using graphics::Emote;
|
||||
@@ -49,7 +47,7 @@ static inline size_t utf8CharLen(uint8_t c)
|
||||
}
|
||||
|
||||
// Remove variation selectors (FE0F) and skin tone modifiers from emoji so they match your labels
|
||||
std::string normalizeEmoji(const std::string &s)
|
||||
static std::string normalizeEmoji(const std::string &s)
|
||||
{
|
||||
std::string out;
|
||||
for (size_t i = 0; i < s.size();) {
|
||||
@@ -82,6 +80,7 @@ uint32_t pauseStart = 0;
|
||||
bool waitingToReset = false;
|
||||
bool scrollStarted = false;
|
||||
static bool didReset = false;
|
||||
static constexpr int MESSAGE_BLOCK_GAP = 6;
|
||||
|
||||
void scrollUp()
|
||||
{
|
||||
@@ -111,22 +110,6 @@ void scrollDown()
|
||||
|
||||
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
|
||||
{
|
||||
std::string renderLine;
|
||||
for (size_t i = 0; i < line.size();) {
|
||||
uint8_t c = (uint8_t)line[i];
|
||||
size_t len = utf8CharLen(c);
|
||||
if (c == 0xEF && i + 2 < line.size() && (uint8_t)line[i + 1] == 0xB8 && (uint8_t)line[i + 2] == 0x8F) {
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
if (c == 0xF0 && i + 3 < line.size() && (uint8_t)line[i + 1] == 0x9F && (uint8_t)line[i + 2] == 0x8F &&
|
||||
((uint8_t)line[i + 3] >= 0xBB && (uint8_t)line[i + 3] <= 0xBF)) {
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
renderLine.append(line, i, len);
|
||||
i += len;
|
||||
}
|
||||
int cursorX = x;
|
||||
const int fontHeight = FONT_HEIGHT_SMALL;
|
||||
|
||||
@@ -203,8 +186,7 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
|
||||
|
||||
// Render the emote (if found)
|
||||
if (matchedEmote && i == nextEmotePos) {
|
||||
// Vertically center emote relative to font baseline (not just midline)
|
||||
int iconY = fontY + (fontHeight - matchedEmote->height) / 2;
|
||||
int iconY = y + (lineHeight - matchedEmote->height) / 2;
|
||||
display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap);
|
||||
cursorX += matchedEmote->width + 1;
|
||||
i += emojiLen;
|
||||
@@ -423,6 +405,63 @@ static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &
|
||||
return totalWidth;
|
||||
}
|
||||
|
||||
struct MessageBlock {
|
||||
size_t start;
|
||||
size_t end;
|
||||
bool mine;
|
||||
};
|
||||
|
||||
static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool isHeaderLine)
|
||||
{
|
||||
if (isHeaderLine) {
|
||||
return lineTopY + (FONT_HEIGHT_SMALL - 1);
|
||||
}
|
||||
|
||||
int tallest = FONT_HEIGHT_SMALL;
|
||||
for (int e = 0; e < numEmotes; ++e) {
|
||||
if (line.find(emotes[e].label) != std::string::npos) {
|
||||
if (emotes[e].height > tallest)
|
||||
tallest = emotes[e].height;
|
||||
}
|
||||
}
|
||||
|
||||
const int lineHeight = std::max(FONT_HEIGHT_SMALL, tallest);
|
||||
const int iconTop = lineTopY + (lineHeight - tallest) / 2;
|
||||
|
||||
return iconTop + tallest - 1;
|
||||
}
|
||||
|
||||
static std::vector<MessageBlock> buildMessageBlocks(const std::vector<bool> &isHeaderVec, const std::vector<bool> &isMineVec)
|
||||
{
|
||||
std::vector<MessageBlock> blocks;
|
||||
if (isHeaderVec.empty())
|
||||
return blocks;
|
||||
|
||||
size_t start = 0;
|
||||
bool mine = isMineVec[0];
|
||||
|
||||
for (size_t i = 1; i < isHeaderVec.size(); ++i) {
|
||||
if (isHeaderVec[i]) {
|
||||
MessageBlock b;
|
||||
b.start = start;
|
||||
b.end = i - 1;
|
||||
b.mine = mine;
|
||||
blocks.push_back(b);
|
||||
|
||||
start = i;
|
||||
mine = isMineVec[i];
|
||||
}
|
||||
}
|
||||
|
||||
MessageBlock last;
|
||||
last.start = start;
|
||||
last.end = isHeaderVec.size() - 1;
|
||||
last.mine = mine;
|
||||
blocks.push_back(last);
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int totalHeight, int scrollOffset, int startY)
|
||||
{
|
||||
if (totalHeight <= visibleHeight)
|
||||
@@ -482,9 +521,18 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
constexpr int LEFT_MARGIN = 2;
|
||||
constexpr int RIGHT_MARGIN = 2;
|
||||
constexpr int SCROLLBAR_WIDTH = 3;
|
||||
constexpr int BUBBLE_PAD_X = 3;
|
||||
constexpr int BUBBLE_PAD_Y = 4;
|
||||
constexpr int BUBBLE_RADIUS = 4;
|
||||
constexpr int BUBBLE_MIN_W = 24;
|
||||
constexpr int BUBBLE_TEXT_INDENT = 2;
|
||||
|
||||
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN;
|
||||
// Check if bubbles are enabled
|
||||
const bool showBubbles = config.display.enable_message_bubbles;
|
||||
const int textIndent = showBubbles ? (BUBBLE_PAD_X + BUBBLE_TEXT_INDENT) : LEFT_MARGIN;
|
||||
|
||||
// Derived widths
|
||||
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (showBubbles ? (BUBBLE_PAD_X * 2) : 0);
|
||||
const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH;
|
||||
|
||||
// Title string depending on mode
|
||||
@@ -547,7 +595,28 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
char chanType[32] = "";
|
||||
if (currentMode == ThreadMode::ALL) {
|
||||
if (m.dest == NODENUM_BROADCAST) {
|
||||
snprintf(chanType, sizeof(chanType), "#%s", channels.getName(m.channelIndex));
|
||||
const char *name = channels.getName(m.channelIndex);
|
||||
if (currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) {
|
||||
if (strcmp(name, "ShortTurbo") == 0)
|
||||
name = "ShortT";
|
||||
else if (strcmp(name, "ShortSlow") == 0)
|
||||
name = "ShortS";
|
||||
else if (strcmp(name, "ShortFast") == 0)
|
||||
name = "ShortF";
|
||||
else if (strcmp(name, "MediumSlow") == 0)
|
||||
name = "MedS";
|
||||
else if (strcmp(name, "MediumFast") == 0)
|
||||
name = "MedF";
|
||||
else if (strcmp(name, "LongSlow") == 0)
|
||||
name = "LongS";
|
||||
else if (strcmp(name, "LongFast") == 0)
|
||||
name = "LongF";
|
||||
else if (strcmp(name, "LongTurbo") == 0)
|
||||
name = "LongT";
|
||||
else if (strcmp(name, "LongMod") == 0)
|
||||
name = "LongM";
|
||||
}
|
||||
snprintf(chanType, sizeof(chanType), "#%s", name);
|
||||
} else {
|
||||
snprintf(chanType, sizeof(chanType), "(DM)");
|
||||
}
|
||||
@@ -614,8 +683,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
}
|
||||
|
||||
// Shrink Sender name if needed
|
||||
int availWidth = SCREEN_WIDTH - display->getStringWidth(timeBuf) - display->getStringWidth(chanType) -
|
||||
display->getStringWidth(" @...") - 10;
|
||||
int availWidth = (mine ? rightTextWidth : leftTextWidth) - display->getStringWidth(timeBuf) -
|
||||
display->getStringWidth(chanType) - display->getStringWidth(" @...");
|
||||
if (availWidth < 0)
|
||||
availWidth = 0;
|
||||
|
||||
@@ -667,6 +736,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
cachedLines = allLines;
|
||||
cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader);
|
||||
|
||||
std::vector<MessageBlock> blocks = buildMessageBlocks(isHeader, isMine);
|
||||
|
||||
// Scrolling logic (unchanged)
|
||||
int totalHeight = 0;
|
||||
for (size_t i = 0; i < cachedHeights.size(); ++i)
|
||||
@@ -714,12 +785,124 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
|
||||
int finalScroll = (int)scrollY;
|
||||
int yOffset = -finalScroll + getTextPositions(display)[1];
|
||||
const int contentTop = getTextPositions(display)[1];
|
||||
const int contentBottom = scrollBottom; // already excludes nav line
|
||||
const int rightEdge = SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN;
|
||||
const int bubbleGapY = std::max(1, MESSAGE_BLOCK_GAP / 2);
|
||||
|
||||
std::vector<int> lineTop;
|
||||
lineTop.resize(cachedLines.size());
|
||||
{
|
||||
int acc = 0;
|
||||
for (size_t i = 0; i < cachedLines.size(); ++i) {
|
||||
lineTop[i] = yOffset + acc;
|
||||
acc += cachedHeights[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Draw bubbles (only if enabled)
|
||||
if (showBubbles) {
|
||||
for (size_t bi = 0; bi < blocks.size(); ++bi) {
|
||||
const auto &b = blocks[bi];
|
||||
if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end)
|
||||
continue;
|
||||
|
||||
int visualTop = lineTop[b.start];
|
||||
|
||||
int topY;
|
||||
if (isHeader[b.start]) {
|
||||
// Header start
|
||||
constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2
|
||||
topY = visualTop - BUBBLE_PAD_TOP_HEADER;
|
||||
} else {
|
||||
// Body start
|
||||
bool thisLineHasEmote = false;
|
||||
for (int e = 0; e < numEmotes; ++e) {
|
||||
if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
|
||||
thisLineHasEmote = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (thisLineHasEmote) {
|
||||
constexpr int EMOTE_PADDING_ABOVE = 4;
|
||||
visualTop -= EMOTE_PADDING_ABOVE;
|
||||
}
|
||||
topY = visualTop - BUBBLE_PAD_Y;
|
||||
}
|
||||
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
|
||||
int bottomY = visualBottom + BUBBLE_PAD_Y;
|
||||
|
||||
if (bi + 1 < blocks.size()) {
|
||||
int nextHeaderIndex = (int)blocks[bi + 1].start;
|
||||
int nextTop = lineTop[nextHeaderIndex];
|
||||
int maxBottom = nextTop - 1 - bubbleGapY;
|
||||
if (bottomY > maxBottom)
|
||||
bottomY = maxBottom;
|
||||
}
|
||||
|
||||
if (bottomY <= topY + 2)
|
||||
continue;
|
||||
|
||||
if (bottomY < contentTop || topY > contentBottom - 1)
|
||||
continue;
|
||||
|
||||
int maxLineW = 0;
|
||||
|
||||
for (size_t i = b.start; i <= b.end; ++i) {
|
||||
int w = 0;
|
||||
if (isHeader[i]) {
|
||||
w = display->getStringWidth(cachedLines[i].c_str());
|
||||
if (b.mine)
|
||||
w += 12; // room for ACK/NACK/relay mark
|
||||
} else {
|
||||
w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
|
||||
}
|
||||
if (w > maxLineW)
|
||||
maxLineW = w;
|
||||
}
|
||||
|
||||
int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (textIndent * 2));
|
||||
int bubbleH = (bottomY - topY) + 1;
|
||||
int bubbleX = 0;
|
||||
if (b.mine) {
|
||||
bubbleX = rightEdge - bubbleW;
|
||||
} else {
|
||||
bubbleX = x;
|
||||
}
|
||||
if (bubbleX < x)
|
||||
bubbleX = x;
|
||||
if (bubbleX + bubbleW > rightEdge)
|
||||
bubbleW = std::max(1, rightEdge - bubbleX);
|
||||
|
||||
// Draw rounded rectangle bubble
|
||||
if (bubbleW > BUBBLE_RADIUS * 2 && bubbleH > BUBBLE_RADIUS * 2) {
|
||||
const int r = BUBBLE_RADIUS;
|
||||
const int bx = bubbleX;
|
||||
const int by = topY;
|
||||
const int bw = bubbleW;
|
||||
const int bh = bubbleH;
|
||||
|
||||
// Draw the 4 corner arcs using drawCircleQuads
|
||||
display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left
|
||||
display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right
|
||||
display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left
|
||||
display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right
|
||||
|
||||
// Draw the 4 edges between corners
|
||||
display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge
|
||||
display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge
|
||||
display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge
|
||||
display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge
|
||||
} else if (bubbleW > 1 && bubbleH > 1) {
|
||||
// Fallback to simple rectangle for very small bubbles
|
||||
display->drawRect(bubbleX, topY, bubbleW, bubbleH);
|
||||
}
|
||||
}
|
||||
} // end if (showBubbles)
|
||||
|
||||
// Render visible lines
|
||||
int lineY = yOffset;
|
||||
for (size_t i = 0; i < cachedLines.size(); ++i) {
|
||||
int lineY = yOffset;
|
||||
for (size_t j = 0; j < i; ++j)
|
||||
lineY += cachedHeights[j];
|
||||
|
||||
if (lineY > -cachedHeights[i] && lineY < scrollBottom) {
|
||||
if (isHeader[i]) {
|
||||
@@ -728,14 +911,28 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
int headerX;
|
||||
if (isMine[i]) {
|
||||
// push header left to avoid overlap with scrollbar
|
||||
headerX = SCREEN_WIDTH - w - SCROLLBAR_WIDTH - RIGHT_MARGIN;
|
||||
headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - (showBubbles ? textIndent : 0);
|
||||
if (headerX < LEFT_MARGIN)
|
||||
headerX = LEFT_MARGIN;
|
||||
} else {
|
||||
headerX = x;
|
||||
headerX = x + textIndent;
|
||||
}
|
||||
display->drawString(headerX, lineY, cachedLines[i].c_str());
|
||||
|
||||
// Draw underline just under header text
|
||||
int underlineY = lineY + FONT_HEIGHT_SMALL;
|
||||
|
||||
int underlineW = w;
|
||||
int maxW = rightEdge - headerX;
|
||||
if (maxW < 0)
|
||||
maxW = 0;
|
||||
if (underlineW > maxW)
|
||||
underlineW = maxW;
|
||||
|
||||
for (int px = 0; px < underlineW; ++px) {
|
||||
display->setPixel(headerX + px, underlineY);
|
||||
}
|
||||
|
||||
// Draw ACK/NACK mark for our own messages
|
||||
if (isMine[i]) {
|
||||
int markX = headerX - 10;
|
||||
@@ -753,32 +950,27 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
// AckStatus::NONE → show nothing
|
||||
}
|
||||
|
||||
// Draw underline just under header text
|
||||
int underlineY = lineY + FONT_HEIGHT_SMALL;
|
||||
for (int px = 0; px < w; ++px) {
|
||||
display->setPixel(headerX + px, underlineY);
|
||||
}
|
||||
} else {
|
||||
// Render message line
|
||||
if (isMine[i]) {
|
||||
// Calculate actual rendered width including emotes
|
||||
int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
|
||||
int rightX = SCREEN_WIDTH - renderedWidth - SCROLLBAR_WIDTH - RIGHT_MARGIN;
|
||||
int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - (showBubbles ? textIndent : 0);
|
||||
if (rightX < LEFT_MARGIN)
|
||||
rightX = LEFT_MARGIN;
|
||||
|
||||
drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes);
|
||||
} else {
|
||||
drawStringWithEmotes(display, x, lineY, cachedLines[i], emotes, numEmotes);
|
||||
drawStringWithEmotes(display, x + textIndent, lineY, cachedLines[i], emotes, numEmotes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lineY += cachedHeights[i];
|
||||
}
|
||||
int totalContentHeight = totalHeight;
|
||||
int visibleHeight = usableHeight;
|
||||
|
||||
// Draw scrollbar
|
||||
drawMessageScrollbar(display, visibleHeight, totalContentHeight, finalScroll, getTextPositions(display)[1]);
|
||||
drawMessageScrollbar(display, usableHeight, totalHeight, finalScroll, getTextPositions(display)[1]);
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
graphics::drawCommonFooter(display, x, y);
|
||||
}
|
||||
@@ -841,7 +1033,6 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
|
||||
constexpr int HEADER_UNDERLINE_GAP = 0; // space between underline and first body line
|
||||
constexpr int HEADER_UNDERLINE_PIX = 1; // underline thickness (1px row drawn)
|
||||
constexpr int BODY_LINE_LEADING = -4; // default vertical leading for normal body lines
|
||||
constexpr int MESSAGE_BLOCK_GAP = 4; // gap after a message block before a new header
|
||||
constexpr int EMOTE_PADDING_ABOVE = 4; // space above emote line (added to line above)
|
||||
constexpr int EMOTE_PADDING_BELOW = 3; // space below emote line (added to emote line)
|
||||
|
||||
@@ -851,6 +1042,7 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
|
||||
for (size_t idx = 0; idx < lines.size(); ++idx) {
|
||||
const auto &line = lines[idx];
|
||||
const int baseHeight = FONT_HEIGHT_SMALL;
|
||||
int lineHeight = baseHeight;
|
||||
|
||||
// Detect if THIS line or NEXT line contains an emote
|
||||
bool hasEmote = false;
|
||||
@@ -872,8 +1064,6 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
|
||||
}
|
||||
}
|
||||
|
||||
int lineHeight = baseHeight;
|
||||
|
||||
if (isHeaderVec[idx]) {
|
||||
// Header line spacing
|
||||
lineHeight = baseHeight + HEADER_UNDERLINE_PIX + HEADER_UNDERLINE_GAP;
|
||||
@@ -922,7 +1112,7 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
|
||||
|
||||
// Banner logic
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from);
|
||||
char longName[48] = "???";
|
||||
char longName[48] = "?";
|
||||
if (node && node->user.long_name) {
|
||||
strncpy(longName, node->user.long_name, sizeof(longName) - 1);
|
||||
longName[sizeof(longName) - 1] = '\0';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#if HAS_SCREEN
|
||||
#include "CompassRenderer.h"
|
||||
#include "GPSStatus.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NodeListRenderer.h"
|
||||
#include "UIRenderer.h"
|
||||
@@ -313,7 +314,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
||||
// === Create the shortName and title string ===
|
||||
const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node";
|
||||
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) ===
|
||||
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) ===
|
||||
// If both are present: "Sig: 97% [2hops]"
|
||||
// If only one: show only that one
|
||||
char signalHopsStr[32] = "";
|
||||
bool haveSignal = false;
|
||||
int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100);
|
||||
int bars = 0;
|
||||
|
||||
// Always use "Sig" for the label
|
||||
const char *signalLabel = " Sig";
|
||||
// Helper to get SNR limit based on modem preset
|
||||
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 ---
|
||||
// If SNR looks reasonable, show signal
|
||||
if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) {
|
||||
snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal);
|
||||
// Only show signal if we have valid SNR
|
||||
if (snr > -100 && snr != 0) {
|
||||
snprintf(signalHopsStr, sizeof(signalHopsStr), "%sSig:%s", leftSideSpacing, qualityLabel);
|
||||
haveSignal = true;
|
||||
}
|
||||
// If hops is valid (>0), show right after signal
|
||||
|
||||
if (node->hops_away > 0) {
|
||||
size_t len = strlen(signalHopsStr);
|
||||
// Decide between "1 Hop" and "N Hops"
|
||||
if (haveSignal) {
|
||||
snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away,
|
||||
(node->hops_away == 1 ? "Hop" : "Hops"));
|
||||
snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [#]");
|
||||
} 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) ===
|
||||
@@ -377,8 +506,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
||||
uint32_t seconds = sinceLastSeen(node);
|
||||
if (seconds != 0 && seconds != UINT32_MAX) {
|
||||
uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
|
||||
// Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago"
|
||||
snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"),
|
||||
// Format as "Heard:Xm ago", "Heard:Xh ago", or "Heard:Xd ago"
|
||||
snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard:?" : "%sHeard:%d%c ago"), leftSideSpacing,
|
||||
(days ? days
|
||||
: hours ? hours
|
||||
: minutes),
|
||||
@@ -386,16 +515,18 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
||||
: hours ? 'h'
|
||||
: 'm'));
|
||||
}
|
||||
if (seenStr[0] && line < 5) {
|
||||
if (seenStr[0]) {
|
||||
display->drawString(x, getTextPositions(display)[line++], seenStr);
|
||||
}
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
// === 4. Uptime (only show if metric is present) ===
|
||||
char uptimeStr[32] = "";
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -422,16 +553,16 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
||||
if (miles < 0.1) {
|
||||
int feet = (int)(miles * 5280);
|
||||
if (feet > 0 && feet < 1000) {
|
||||
snprintf(distStr, sizeof(distStr), " Distance: %dft", feet);
|
||||
snprintf(distStr, sizeof(distStr), "%sDistance:%dft", leftSideSpacing, feet);
|
||||
haveDistance = true;
|
||||
} else if (feet >= 1000) {
|
||||
snprintf(distStr, sizeof(distStr), " Distance: ¼mi");
|
||||
snprintf(distStr, sizeof(distStr), "%sDistance:¼mi", leftSideSpacing);
|
||||
haveDistance = true;
|
||||
}
|
||||
} else {
|
||||
int roundedMiles = (int)(miles + 0.5);
|
||||
if (roundedMiles > 0 && roundedMiles < 1000) {
|
||||
snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles);
|
||||
snprintf(distStr, sizeof(distStr), "%sDistance:%dmi", leftSideSpacing, roundedMiles);
|
||||
haveDistance = true;
|
||||
}
|
||||
}
|
||||
@@ -439,26 +570,74 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
||||
if (distanceKm < 1.0) {
|
||||
int meters = (int)(distanceKm * 1000);
|
||||
if (meters > 0 && meters < 1000) {
|
||||
snprintf(distStr, sizeof(distStr), " Distance: %dm", meters);
|
||||
snprintf(distStr, sizeof(distStr), "%sDistance:%dm", leftSideSpacing, meters);
|
||||
haveDistance = true;
|
||||
} else if (meters >= 1000) {
|
||||
snprintf(distStr, sizeof(distStr), " Distance: 1km");
|
||||
snprintf(distStr, sizeof(distStr), "%sDistance:1km", leftSideSpacing);
|
||||
haveDistance = true;
|
||||
}
|
||||
} else {
|
||||
int km = (int)(distanceKm + 0.5);
|
||||
if (km > 0 && km < 1000) {
|
||||
snprintf(distStr, sizeof(distStr), " Distance: %dkm", km);
|
||||
snprintf(distStr, sizeof(distStr), "%sDistance:%dkm", leftSideSpacing, km);
|
||||
haveDistance = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only display if we actually have a value!
|
||||
if (haveDistance && distStr[0] && line < 5) {
|
||||
if (haveDistance && distStr[0]) {
|
||||
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 ---
|
||||
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
|
||||
bool showCompass = false;
|
||||
@@ -593,7 +772,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
}
|
||||
char uptimeStr[32] = "";
|
||||
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);
|
||||
|
||||
@@ -984,7 +1163,6 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
config.display.heading_bold = false;
|
||||
|
||||
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.fixed_position) {
|
||||
@@ -1029,10 +1207,10 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
char uptimeStr[32];
|
||||
#if defined(USE_EINK)
|
||||
// E-Ink: skip seconds, show only days/hours/mins
|
||||
getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), false);
|
||||
getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), false);
|
||||
#else
|
||||
// Non E-Ink: include seconds where useful
|
||||
getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), true);
|
||||
getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), true);
|
||||
#endif
|
||||
|
||||
display->drawString(0, getTextPositions(display)[line++], uptimeStr);
|
||||
|
||||
@@ -83,6 +83,12 @@ static const unsigned char mail[] PROGMEM = {
|
||||
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
|
||||
const uint8_t icon_mail[] PROGMEM = {
|
||||
0b11111111, // ████████ top border
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#include "graphics/niche/InkHUD/Tile.h"
|
||||
#include <cstdint>
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./Applet.h"
|
||||
@@ -55,7 +57,7 @@ InkHUD::Tile *InkHUD::Applet::getTile()
|
||||
}
|
||||
|
||||
// Draw the applet
|
||||
void InkHUD::Applet::render()
|
||||
void InkHUD::Applet::render(bool full)
|
||||
{
|
||||
assert(assignedTile); // Ensure that we have a 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
|
||||
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.
|
||||
wantFullRender = true; // Default to a full render
|
||||
|
||||
updateDimensions();
|
||||
resetDrawingSpace();
|
||||
onRender(); // Derived applet's drawing takes place here
|
||||
onRender(full); // Draw the applet
|
||||
|
||||
// Handle "Tile Highlighting"
|
||||
// Some devices may use an auxiliary button to switch between tiles
|
||||
@@ -115,6 +118,11 @@ Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType()
|
||||
return wantUpdateType;
|
||||
}
|
||||
|
||||
bool InkHUD::Applet::wantsFullRender()
|
||||
{
|
||||
return wantFullRender;
|
||||
}
|
||||
|
||||
// Get size of the applet's drawing space from its tile
|
||||
// Performed immediately before derived applet's drawing code runs
|
||||
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,
|
||||
// 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
|
||||
void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type)
|
||||
void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type, bool full)
|
||||
{
|
||||
wantRender = true;
|
||||
wantUpdateType = type;
|
||||
wantFullRender = full;
|
||||
inkhud->requestUpdate();
|
||||
}
|
||||
|
||||
@@ -778,6 +787,16 @@ void InkHUD::Applet::drawHeader(std::string text)
|
||||
drawPixel(x, 0, BLACK);
|
||||
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
|
||||
|
||||
@@ -64,10 +64,11 @@ class Applet : public GFX
|
||||
|
||||
// Rendering
|
||||
|
||||
void render(); // Draw the applet
|
||||
void render(bool full); // Draw the applet
|
||||
bool wantsToRender(); // Check whether applet wants to render
|
||||
bool wantsToAutoshow(); // Check whether applet wants to become foreground
|
||||
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 resetDrawingSpace(); // Makes sure every render starts with same parameters
|
||||
|
||||
@@ -82,7 +83,7 @@ class Applet : public GFX
|
||||
|
||||
// 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 onDeactivate() {}
|
||||
virtual void onForeground() {}
|
||||
@@ -96,6 +97,9 @@ class Applet : public GFX
|
||||
virtual void onNavDown() {}
|
||||
virtual void onNavLeft() {}
|
||||
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
|
||||
|
||||
@@ -108,8 +112,9 @@ class Applet : public GFX
|
||||
protected:
|
||||
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 requestAutoshow(); // Ask for applet to be moved to foreground
|
||||
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED,
|
||||
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 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?
|
||||
NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType =
|
||||
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::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::MapApplet::onRender()
|
||||
void InkHUD::MapApplet::onRender(bool full)
|
||||
{
|
||||
// Abort if no markers to render
|
||||
if (!enoughMarkers()) {
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace NicheGraphics::InkHUD
|
||||
class MapApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
protected:
|
||||
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
|
||||
void InkHUD::NodeListApplet::onRender()
|
||||
void InkHUD::NodeListApplet::onRender(bool full)
|
||||
{
|
||||
|
||||
// ================================
|
||||
|
||||
@@ -46,7 +46,7 @@ class NodeListApplet : public Applet, public MeshModule
|
||||
public:
|
||||
NodeListApplet(const char *name);
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
bool wantPacket(const meshtastic_MeshPacket *p) override;
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
@@ -6,7 +6,7 @@ using namespace NicheGraphics;
|
||||
|
||||
// All drawing happens here
|
||||
// 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!");
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class BasicExampleApplet : public Applet
|
||||
// You must have an onRender() method
|
||||
// All drawing happens here
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -35,7 +35,7 @@ ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_Mesh
|
||||
// We can trigger a render by calling requestUpdate()
|
||||
// Render might be called by some external source
|
||||
// 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)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class NewMsgExampleApplet : public Applet, public SinglePortModule
|
||||
NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {}
|
||||
|
||||
// All drawing happens here
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
// Your applet might also want to use some of these
|
||||
// Useful for setting up or tidying up
|
||||
|
||||
@@ -10,7 +10,7 @@ InkHUD::AlignStickApplet::AlignStickApplet()
|
||||
bringToForeground();
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onRender()
|
||||
void InkHUD::AlignStickApplet::onRender(bool full)
|
||||
{
|
||||
setFont(fontMedium);
|
||||
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
|
||||
// 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()
|
||||
{
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onExitLong()
|
||||
{
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onNavUp()
|
||||
@@ -172,7 +170,6 @@ void InkHUD::AlignStickApplet::onNavUp()
|
||||
settings->joystick.aligned = true;
|
||||
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onNavDown()
|
||||
@@ -181,7 +178,6 @@ void InkHUD::AlignStickApplet::onNavDown()
|
||||
settings->joystick.aligned = true;
|
||||
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onNavLeft()
|
||||
@@ -190,7 +186,6 @@ void InkHUD::AlignStickApplet::onNavLeft()
|
||||
settings->joystick.aligned = true;
|
||||
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onNavRight()
|
||||
@@ -199,7 +194,6 @@ void InkHUD::AlignStickApplet::onNavRight()
|
||||
settings->joystick.aligned = true;
|
||||
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -23,7 +23,7 @@ class AlignStickApplet : public SystemApplet
|
||||
public:
|
||||
AlignStickApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonLongPress() override;
|
||||
|
||||
@@ -6,6 +6,8 @@ using namespace NicheGraphics;
|
||||
|
||||
InkHUD::BatteryIconApplet::BatteryIconApplet()
|
||||
{
|
||||
alwaysRender = true; // render everytime the screen is updated
|
||||
|
||||
// Show at boot, if user has previously enabled the feature
|
||||
if (settings->optionalFeatures.batteryIcon)
|
||||
bringToForeground();
|
||||
@@ -44,39 +46,29 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
|
||||
return 0; // Tell Observable to continue informing other observers
|
||||
}
|
||||
|
||||
void InkHUD::BatteryIconApplet::onRender()
|
||||
void InkHUD::BatteryIconApplet::onRender(bool full)
|
||||
{
|
||||
// Fill entire tile
|
||||
// - 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
|
||||
// Clear the region beneath the tile, including the border
|
||||
// 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"
|
||||
fillRect(l, t, w, h, WHITE);
|
||||
|
||||
// Vertical centerline
|
||||
const int16_t m = t + (h / 2);
|
||||
fillRect(0, 0, width(), height(), WHITE);
|
||||
|
||||
// =====================
|
||||
// Draw battery outline
|
||||
// =====================
|
||||
|
||||
// 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;
|
||||
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);
|
||||
|
||||
// Main body of battery
|
||||
const int16_t bodyL = bumpL + bumpW;
|
||||
const int16_t &bodyT = t;
|
||||
const int16_t &bodyH = h;
|
||||
const int16_t bodyW = w - bumpW;
|
||||
const int16_t bodyL = 1 + bumpW;
|
||||
const int16_t &bodyT = 1;
|
||||
const int16_t &bodyH = height() - 2; // Handle top/bottom padding
|
||||
const int16_t bodyW = (width() - 1) - bumpW; // Handle 1px left pad
|
||||
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
|
||||
|
||||
// Erase join between bump and body
|
||||
@@ -87,12 +79,13 @@ void InkHUD::BatteryIconApplet::onRender()
|
||||
// ===================
|
||||
|
||||
constexpr int16_t slicePad = 2;
|
||||
const int16_t sliceL = bodyL + slicePad;
|
||||
int16_t sliceL = bodyL + slicePad;
|
||||
const int16_t sliceT = bodyT + slicePad;
|
||||
const uint16_t sliceH = bodyH - (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);
|
||||
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
|
||||
|
||||
@@ -23,7 +23,7 @@ class BatteryIconApplet : public SystemApplet
|
||||
public:
|
||||
BatteryIconApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
void InkHUD::LogoApplet::onRender()
|
||||
void InkHUD::LogoApplet::onRender(bool full)
|
||||
{
|
||||
// Size of the region which the logo should "scale to fit"
|
||||
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
|
||||
// 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
|
||||
@@ -138,10 +138,10 @@ void InkHUD::LogoApplet::onShutdown()
|
||||
// Intention is to restore display health.
|
||||
|
||||
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.
|
||||
inverted = false;
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, false);
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, true, false);
|
||||
delay(1000); // Cooldown
|
||||
|
||||
// Prepare for the powered-off screen now
|
||||
@@ -155,6 +155,18 @@ void InkHUD::LogoApplet::onShutdown()
|
||||
// This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is complete
|
||||
}
|
||||
|
||||
void InkHUD::LogoApplet::onApplyingChanges()
|
||||
{
|
||||
bringToForeground();
|
||||
|
||||
textLeft = "";
|
||||
textRight = "";
|
||||
textTitle = "Applying changes";
|
||||
fontTitle = fontSmall;
|
||||
|
||||
inkhud->forceUpdate(Drivers::EInk::FAST, false);
|
||||
}
|
||||
|
||||
void InkHUD::LogoApplet::onReboot()
|
||||
{
|
||||
bringToForeground();
|
||||
@@ -164,7 +176,7 @@ void InkHUD::LogoApplet::onReboot()
|
||||
textTitle = "Rebooting...";
|
||||
fontTitle = fontSmall;
|
||||
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, false);
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, true, false);
|
||||
// Perform the update right now, waiting here until complete
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,12 @@ class LogoApplet : public SystemApplet, public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
LogoApplet();
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onShutdown() override;
|
||||
void onReboot() override;
|
||||
void onApplyingChanges();
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override;
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace NicheGraphics::InkHUD
|
||||
enum MenuAction {
|
||||
NO_ACTION,
|
||||
SEND_PING,
|
||||
FREE_TEXT,
|
||||
STORE_CANNEDMESSAGE_SELECTION,
|
||||
SEND_CANNEDMESSAGE,
|
||||
SHUTDOWN,
|
||||
@@ -36,6 +37,84 @@ enum MenuAction {
|
||||
TOGGLE_NOTIFICATIONS,
|
||||
TOGGLE_INVERT_COLOR,
|
||||
TOGGLE_12H_CLOCK,
|
||||
// Regions
|
||||
SET_REGION_US,
|
||||
SET_REGION_EU_868,
|
||||
SET_REGION_EU_433,
|
||||
SET_REGION_CN,
|
||||
SET_REGION_JP,
|
||||
SET_REGION_ANZ,
|
||||
SET_REGION_KR,
|
||||
SET_REGION_TW,
|
||||
SET_REGION_RU,
|
||||
SET_REGION_IN,
|
||||
SET_REGION_NZ_865,
|
||||
SET_REGION_TH,
|
||||
SET_REGION_LORA_24,
|
||||
SET_REGION_UA_433,
|
||||
SET_REGION_UA_868,
|
||||
SET_REGION_MY_433,
|
||||
SET_REGION_MY_919,
|
||||
SET_REGION_SG_923,
|
||||
SET_REGION_PH_433,
|
||||
SET_REGION_PH_868,
|
||||
SET_REGION_PH_915,
|
||||
SET_REGION_ANZ_433,
|
||||
SET_REGION_KZ_433,
|
||||
SET_REGION_KZ_863,
|
||||
SET_REGION_NP_865,
|
||||
SET_REGION_BR_902,
|
||||
// Device Roles
|
||||
SET_ROLE_CLIENT,
|
||||
SET_ROLE_CLIENT_MUTE,
|
||||
SET_ROLE_ROUTER,
|
||||
SET_ROLE_REPEATER,
|
||||
// Presets
|
||||
SET_PRESET_LONG_SLOW,
|
||||
SET_PRESET_LONG_MODERATE,
|
||||
SET_PRESET_LONG_FAST,
|
||||
SET_PRESET_MEDIUM_SLOW,
|
||||
SET_PRESET_MEDIUM_FAST,
|
||||
SET_PRESET_SHORT_SLOW,
|
||||
SET_PRESET_SHORT_FAST,
|
||||
SET_PRESET_SHORT_TURBO,
|
||||
// Timezones
|
||||
SET_TZ_US_HAWAII,
|
||||
SET_TZ_US_ALASKA,
|
||||
SET_TZ_US_PACIFIC,
|
||||
SET_TZ_US_ARIZONA,
|
||||
SET_TZ_US_MOUNTAIN,
|
||||
SET_TZ_US_CENTRAL,
|
||||
SET_TZ_US_EASTERN,
|
||||
SET_TZ_BR_BRAZILIA,
|
||||
SET_TZ_UTC,
|
||||
SET_TZ_EU_WESTERN,
|
||||
SET_TZ_EU_CENTRAL,
|
||||
SET_TZ_EU_EASTERN,
|
||||
SET_TZ_ASIA_KOLKATA,
|
||||
SET_TZ_ASIA_HONG_KONG,
|
||||
SET_TZ_AU_AWST,
|
||||
SET_TZ_AU_ACST,
|
||||
SET_TZ_AU_AEST,
|
||||
SET_TZ_PACIFIC_NZ,
|
||||
// Power
|
||||
TOGGLE_POWER_SAVE,
|
||||
CALIBRATE_ADC,
|
||||
// Bluetooth
|
||||
TOGGLE_BLUETOOTH,
|
||||
TOGGLE_BLUETOOTH_PAIR_MODE,
|
||||
// Channel
|
||||
TOGGLE_CHANNEL_UPLINK,
|
||||
TOGGLE_CHANNEL_DOWNLINK,
|
||||
TOGGLE_CHANNEL_POSITION,
|
||||
SET_CHANNEL_PRECISION,
|
||||
// Display
|
||||
TOGGLE_DISPLAY_UNITS,
|
||||
// Network
|
||||
TOGGLE_WIFI,
|
||||
// Administration
|
||||
RESET_NODEDB_ALL,
|
||||
RESET_NODEDB_KEEP_FAVORITES,
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user