mirror of
https://github.com/meshtastic/firmware.git
synced 2026-02-04 16:11:54 +00:00
Compare commits
58 Commits
fix-radio-
...
m5-shutdow
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de731b8286 | ||
|
|
d8e1553cef | ||
|
|
61147977b2 | ||
|
|
840b08bb9d | ||
|
|
e205d8de0d | ||
|
|
4d9b718374 | ||
|
|
e9d4485bb5 | ||
|
|
7b03980e0a | ||
|
|
004179c045 | ||
|
|
8f630bfcf3 | ||
|
|
7bbfe99fbe | ||
|
|
caae6bc597 | ||
|
|
200e79e800 | ||
|
|
c19fc62683 | ||
|
|
4cf01e7e53 | ||
|
|
ad4b1d9c2b | ||
|
|
68733a6c51 | ||
|
|
22617076f8 | ||
|
|
6f5a7672b4 | ||
|
|
e08c050720 | ||
|
|
28b4f37a93 | ||
|
|
5dd06edd00 | ||
|
|
eeb7373043 | ||
|
|
dbded86dcb | ||
|
|
45fbc0f9d3 | ||
|
|
61b39acc7d | ||
|
|
8af9e7fbdc | ||
|
|
1f7ed6888a | ||
|
|
31bf51b3f2 | ||
|
|
334a4f04cd | ||
|
|
b18742c211 | ||
|
|
03084f6d3b | ||
|
|
94d7b71aa8 | ||
|
|
415686dd06 | ||
|
|
b2f2f6b305 | ||
|
|
df400850c1 | ||
|
|
6ab2f02dbc | ||
|
|
d7d6fe7f0f | ||
|
|
d44ceb6eb2 | ||
|
|
4fd0a8276b | ||
|
|
1d219a93ab | ||
|
|
f710cd6ecb | ||
|
|
571c1ac34c | ||
|
|
fb635987d1 | ||
|
|
a922751afc | ||
|
|
c1e3f56324 | ||
|
|
d0562e1ee6 | ||
|
|
4eb4c4b584 | ||
|
|
69a42e1fd2 | ||
|
|
fd498bebad | ||
|
|
23a8b5a66f | ||
|
|
e1e8d6124d | ||
|
|
10b2eae70c | ||
|
|
cfda9bb8ef | ||
|
|
d1edd386b6 | ||
|
|
b6a1020fc5 | ||
|
|
91ad861b26 | ||
|
|
c8079d4115 |
213
.github/workflows/models_issue_triage.yml
vendored
Normal file
213
.github/workflows/models_issue_triage.yml
vendored
Normal file
@@ -0,0 +1,213 @@
|
||||
name: Issue Triage (Models)
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
models: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
if: ${{ github.repository == 'meshtastic/firmware' && github.event.issue.user.type != 'Bot' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Step 1: Quality check (spam/AI-slop detection) - runs first, exits early if spam
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
- name: Detect spam or low-quality content
|
||||
uses: actions/ai-inference@v2
|
||||
id: quality
|
||||
continue-on-error: true
|
||||
with:
|
||||
max-tokens: 20
|
||||
prompt: |
|
||||
Is this GitHub issue spam, AI-generated slop, or low quality?
|
||||
|
||||
Title: ${{ github.event.issue.title }}
|
||||
Body: ${{ github.event.issue.body }}
|
||||
|
||||
Respond with exactly one of: spam, ai-generated, needs-review, ok
|
||||
system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop.
|
||||
model: openai/gpt-4o-mini
|
||||
|
||||
- name: Apply quality label if needed
|
||||
if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
|
||||
with:
|
||||
script: |
|
||||
const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase();
|
||||
const labelMeta = {
|
||||
'spam': { color: 'd73a4a', description: 'Possible spam' },
|
||||
'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' },
|
||||
'needs-review': { color: 'f9d0c4', description: 'Needs human review' },
|
||||
};
|
||||
const meta = labelMeta[label];
|
||||
if (!meta) return;
|
||||
|
||||
// Ensure label exists
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
||||
}
|
||||
|
||||
// Apply label
|
||||
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] });
|
||||
|
||||
// Set output to skip remaining steps
|
||||
core.setOutput('is_spam', 'true');
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Step 2: Duplicate detection - only if not spam
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
- name: Detect duplicate issues
|
||||
if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
|
||||
uses: pelikhan/action-genai-issue-dedup@bdb3b5d9451c1090ffcdf123d7447a5e7c7a2528 # v0.0.19
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Step 3: Completeness check + auto-labeling (combined into one AI call)
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
- name: Determine if completeness check should be skipped
|
||||
if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
|
||||
uses: actions/github-script@v8
|
||||
id: check-skip
|
||||
with:
|
||||
script: |
|
||||
const title = (context.payload.issue.title || '').toLowerCase();
|
||||
const labels = (context.payload.issue.labels || []).map(label => label.name);
|
||||
const hasFeatureRequest = title.includes('feature request');
|
||||
const hasEnhancement = labels.includes('enhancement');
|
||||
const shouldSkip = hasFeatureRequest && hasEnhancement;
|
||||
core.setOutput('should_skip', shouldSkip ? 'true' : 'false');
|
||||
|
||||
- name: Analyze issue completeness and determine labels
|
||||
if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true'
|
||||
uses: actions/ai-inference@v2
|
||||
id: analysis
|
||||
continue-on-error: true
|
||||
with:
|
||||
prompt: |
|
||||
Analyze this GitHub issue for completeness and determine if it needs labels.
|
||||
|
||||
IMPORTANT: Distinguish between:
|
||||
- Device/firmware bugs (crashes, reboots, lockups, radio/GPS/display/power issues) - these need device logs
|
||||
- Build/release/packaging issues (missing files, CI failures, download problems) - these do NOT need device logs
|
||||
- Documentation or website issues - these do NOT need device logs
|
||||
|
||||
If this is a device/firmware bug, request device logs and explain how to get them:
|
||||
|
||||
Web Flasher logs:
|
||||
- Go to https://flasher.meshtastic.org
|
||||
- Connect the device via USB and click Connect
|
||||
- Open the device console/log output, reproduce the problem, then copy/download and attach/paste the logs
|
||||
|
||||
Meshtastic CLI logs:
|
||||
- Run: meshtastic --port <serial-port> --noproto
|
||||
- Reproduce the problem, then copy/paste the terminal output
|
||||
|
||||
Also request key context if missing: device model/variant, firmware version, region, steps to reproduce, expected vs actual.
|
||||
|
||||
Respond ONLY with valid JSON (no markdown, no code fences):
|
||||
{"complete": true, "comment": "", "label": "none"}
|
||||
OR
|
||||
{"complete": false, "comment": "Your helpful comment", "label": "needs-logs"}
|
||||
|
||||
Use "needs-logs" ONLY if this is a device/firmware bug AND no logs are attached.
|
||||
Use "needs-info" if basic info like firmware version or steps to reproduce are missing.
|
||||
Use "none" if the issue is complete, is a feature request, or is a build/CI/packaging issue.
|
||||
|
||||
Title: ${{ github.event.issue.title }}
|
||||
Body: ${{ github.event.issue.body }}
|
||||
system-prompt: You are a helpful assistant that triages GitHub issues. Be conservative with labels. Only request device logs for actual device/firmware bugs, not for build/release/CI issues.
|
||||
model: openai/gpt-4o-mini
|
||||
|
||||
- name: Process analysis result
|
||||
if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != ''
|
||||
uses: actions/github-script@v8
|
||||
id: process
|
||||
env:
|
||||
AI_RESPONSE: ${{ steps.analysis.outputs.response }}
|
||||
with:
|
||||
script: |
|
||||
let raw = (process.env.AI_RESPONSE || '').trim();
|
||||
|
||||
// Strip markdown code fences if present
|
||||
raw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
|
||||
|
||||
let complete = true;
|
||||
let comment = '';
|
||||
let label = 'none';
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
complete = !!parsed.complete;
|
||||
comment = (parsed.comment ?? '').toString().trim();
|
||||
label = (parsed.label ?? 'none').toString().trim().toLowerCase();
|
||||
} catch {
|
||||
// If JSON parse fails, log warning and don't comment (avoid posting raw JSON)
|
||||
console.log('Failed to parse AI response as JSON:', raw);
|
||||
complete = true;
|
||||
comment = '';
|
||||
label = 'none';
|
||||
}
|
||||
|
||||
// Validate label
|
||||
const allowedLabels = new Set(['needs-logs', 'needs-info', 'none']);
|
||||
if (!allowedLabels.has(label)) label = 'none';
|
||||
|
||||
// Only comment if we have a valid parsed comment (not raw JSON)
|
||||
const shouldComment = !complete && comment.length > 0 && !comment.startsWith('{');
|
||||
core.setOutput('should_comment', shouldComment ? 'true' : 'false');
|
||||
core.setOutput('comment_body', comment);
|
||||
core.setOutput('label', label);
|
||||
|
||||
- name: Apply triage label
|
||||
if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none'
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
LABEL_NAME: ${{ steps.process.outputs.label }}
|
||||
with:
|
||||
script: |
|
||||
const label = process.env.LABEL_NAME;
|
||||
const labelMeta = {
|
||||
'needs-logs': { color: 'cfd3d7', description: 'Device logs requested for triage' },
|
||||
'needs-info': { color: 'f9d0c4', description: 'More information requested for triage' },
|
||||
};
|
||||
const meta = labelMeta[label];
|
||||
if (!meta) return;
|
||||
|
||||
// Ensure label exists
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
||||
}
|
||||
|
||||
// Apply label
|
||||
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] });
|
||||
|
||||
- name: Comment on issue
|
||||
if: steps.process.outputs.should_comment == 'true'
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
COMMENT_BODY: ${{ steps.process.outputs.comment_body }}
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.issue.number,
|
||||
body: process.env.COMMENT_BODY
|
||||
});
|
||||
139
.github/workflows/models_pr_triage.yml
vendored
Normal file
139
.github/workflows/models_pr_triage.yml
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
name: PR Triage (Models)
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
models: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
if: ${{ github.repository == 'meshtastic/firmware' && github.event.pull_request.user.type != 'Bot' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Step 1: Check if PR already has automation/type labels (skip if so)
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
- name: Check existing labels
|
||||
uses: actions/github-script@v8
|
||||
id: check-labels
|
||||
with:
|
||||
script: |
|
||||
const skipLabels = new Set(['automation']);
|
||||
const typeLabels = new Set(['bugfix', 'hardware-support', 'enhancement', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']);
|
||||
const prLabels = context.payload.pull_request.labels.map(l => l.name);
|
||||
|
||||
const shouldSkipAll = prLabels.some(l => skipLabels.has(l));
|
||||
const hasTypeLabel = prLabels.some(l => typeLabels.has(l));
|
||||
|
||||
core.setOutput('skip_all', shouldSkipAll ? 'true' : 'false');
|
||||
core.setOutput('has_type_label', hasTypeLabel ? 'true' : 'false');
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Step 2: Quality check (spam/AI-slop detection)
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
- name: Detect spam or low-quality content
|
||||
if: steps.check-labels.outputs.skip_all != 'true'
|
||||
uses: actions/ai-inference@v2
|
||||
id: quality
|
||||
continue-on-error: true
|
||||
with:
|
||||
max-tokens: 20
|
||||
prompt: |
|
||||
Is this GitHub pull request spam, AI-generated slop, or low quality?
|
||||
|
||||
Title: ${{ github.event.pull_request.title }}
|
||||
Body: ${{ github.event.pull_request.body }}
|
||||
|
||||
Respond with exactly one of: spam, ai-generated, needs-review, ok
|
||||
system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop.
|
||||
model: openai/gpt-4o-mini
|
||||
|
||||
- name: Apply quality label if needed
|
||||
if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
|
||||
uses: actions/github-script@v8
|
||||
id: quality-label
|
||||
env:
|
||||
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
|
||||
with:
|
||||
script: |
|
||||
const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase();
|
||||
const labelMeta = {
|
||||
'spam': { color: 'd73a4a', description: 'Possible spam' },
|
||||
'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' },
|
||||
'needs-review': { color: 'f9d0c4', description: 'Needs human review' },
|
||||
};
|
||||
const meta = labelMeta[label];
|
||||
if (!meta) return;
|
||||
|
||||
// Ensure label exists
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
||||
}
|
||||
|
||||
// Apply label
|
||||
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] });
|
||||
|
||||
core.setOutput('is_spam', 'true');
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Step 3: Auto-label PR type (bugfix/hardware-support/enhancement)
|
||||
# Only skip for spam/ai-generated; still classify needs-review PRs
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
- name: Classify PR for labeling
|
||||
if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.quality.outputs.response != 'spam' && steps.quality.outputs.response != 'ai-generated'
|
||||
uses: actions/ai-inference@v2
|
||||
id: classify
|
||||
continue-on-error: true
|
||||
with:
|
||||
max-tokens: 30
|
||||
prompt: |
|
||||
Classify this pull request into exactly one category.
|
||||
|
||||
Return exactly one of: bugfix, hardware-support, enhancement
|
||||
|
||||
Use bugfix if it fixes a bug, crash, or incorrect behavior.
|
||||
Use hardware-support if it adds or improves support for a specific hardware device/variant.
|
||||
Use enhancement if it adds a new feature, improves performance, or refactors code.
|
||||
|
||||
Title: ${{ github.event.pull_request.title }}
|
||||
Body: ${{ github.event.pull_request.body }}
|
||||
system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label.
|
||||
model: openai/gpt-4o-mini
|
||||
|
||||
- name: Apply type label
|
||||
if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != ''
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
TYPE_LABEL: ${{ steps.classify.outputs.response }}
|
||||
with:
|
||||
script: |
|
||||
const label = (process.env.TYPE_LABEL || '').trim().toLowerCase();
|
||||
const labelMeta = {
|
||||
'bugfix': { color: 'd73a4a', description: 'Bug fix' },
|
||||
'hardware-support': { color: '0e8a16', description: 'Hardware support addition or improvement' },
|
||||
'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' },
|
||||
};
|
||||
const meta = labelMeta[label];
|
||||
if (!meta) return;
|
||||
|
||||
// Ensure label exists
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
||||
}
|
||||
|
||||
// Apply label
|
||||
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] });
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -50,3 +50,6 @@ idf_component.yml
|
||||
CMakeLists.txt
|
||||
/sdkconfig.*
|
||||
.dummy/*
|
||||
|
||||
# PYTHONPATH used by the Nix shell
|
||||
.python3
|
||||
|
||||
23
bin/config.d/lora-usb-umesh-1262-30dbm.yaml
Normal file
23
bin/config.d/lora-usb-umesh-1262-30dbm.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
Lora:
|
||||
Module: sx1262
|
||||
CS: 0
|
||||
IRQ: 6
|
||||
Reset: 1
|
||||
Busy: 4
|
||||
RXen: 2
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: ch341
|
||||
USB_PID: 0x5512
|
||||
USB_VID: 0x1A86
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# USB_Serialnum: 12345678
|
||||
SX126X_MAX_POWER: 22
|
||||
# Reduce output power to improve EMI
|
||||
NUM_PA_POINTS: 22
|
||||
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
|
||||
# Note: This module integrates an additional PA to achieve higher output power.
|
||||
# The 'power' parameter here does not represent the actual RF output.
|
||||
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
|
||||
# Each array element corresponds to the additional gain when that input level is set,
|
||||
# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index].
|
||||
# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information.
|
||||
@@ -1,15 +0,0 @@
|
||||
Lora:
|
||||
Module: sx1262
|
||||
CS: 0
|
||||
IRQ: 6
|
||||
Reset: 1
|
||||
Busy: 4
|
||||
RXen: 2
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: ch341
|
||||
USB_PID: 0x5512
|
||||
USB_VID: 0x1A86
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# USB_Serialnum: 12345678
|
||||
SX126X_MAX_POWER: 30
|
||||
# Reduce output power to improve EMI
|
||||
23
bin/config.d/lora-usb-umesh-1268-30dbm.yaml
Normal file
23
bin/config.d/lora-usb-umesh-1268-30dbm.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
Lora:
|
||||
Module: sx1268
|
||||
CS: 0
|
||||
IRQ: 6
|
||||
Reset: 1
|
||||
Busy: 4
|
||||
RXen: 2
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: ch341
|
||||
USB_PID: 0x5512
|
||||
USB_VID: 0x1A86
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# USB_Serialnum: 12345678
|
||||
SX126X_MAX_POWER: 22
|
||||
# Reduce output power to improve EMI
|
||||
NUM_PA_POINTS: 22
|
||||
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
|
||||
# Note: This module integrates an additional PA to achieve higher output power.
|
||||
# The 'power' parameter here does not represent the actual RF output.
|
||||
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
|
||||
# Each array element corresponds to the additional gain when that input level is set,
|
||||
# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index].
|
||||
# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information.
|
||||
@@ -1,15 +0,0 @@
|
||||
Lora:
|
||||
Module: sx1268
|
||||
CS: 0
|
||||
IRQ: 6
|
||||
Reset: 1
|
||||
Busy: 4
|
||||
RXen: 2
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: ch341
|
||||
USB_PID: 0x5512
|
||||
USB_VID: 0x1A86
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# USB_Serialnum: 12345678
|
||||
SX126X_MAX_POWER: 30
|
||||
# Reduce output power to improve EMI
|
||||
@@ -156,16 +156,8 @@ IF %BPS_RESET% EQU 1 (
|
||||
SET "PROGNAME=!FILENAME:.factory.bin=!"
|
||||
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.
|
||||
|
||||
@@ -131,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"
|
||||
|
||||
44
flake.lock
generated
Normal file
44
flake.lock
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1767039857,
|
||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||
"owner": "NixOS",
|
||||
"repo": "flake-compat",
|
||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1766314097,
|
||||
"narHash": "sha256-laJftWbghBehazn/zxVJ8NdENVgjccsWAdAqKXhErrM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "306ea70f9eb0fb4e040f8540e2deab32ed7e2055",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
66
flake.nix
Normal file
66
flake.nix
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
description = "Nix flake to compile Meshtastic firmware";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
|
||||
# Shim to make flake.nix work with stable Nix.
|
||||
flake-compat = {
|
||||
url = "github:NixOS/flake-compat";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs:
|
||||
let
|
||||
lib = inputs.nixpkgs.lib;
|
||||
|
||||
forAllSystems =
|
||||
fn:
|
||||
lib.genAttrs lib.systems.flakeExposed (
|
||||
system:
|
||||
fn {
|
||||
pkgs = import inputs.nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
inherit system;
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
devShells = forAllSystems (
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
python3 = pkgs.python312.withPackages (
|
||||
ps: with ps; [
|
||||
google
|
||||
]
|
||||
);
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
python3
|
||||
platformio
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
# Set up PlatformIO to use a local core directory.
|
||||
export PLATFORMIO_CORE_DIR=$PWD/.platformio
|
||||
# Tell pip to put packages into $PIP_PREFIX instead of the usual
|
||||
# location. This is especially necessary under NixOS to avoid having
|
||||
# pip trying to write to the read-only Nix store. For more info,
|
||||
# see https://wiki.nixos.org/wiki/Python
|
||||
export PIP_PREFIX=$PWD/.python3
|
||||
export PYTHONPATH="$PIP_PREFIX/${python3.sitePackages}"
|
||||
export PATH="$PIP_PREFIX/bin:$PATH"
|
||||
# Avoids reproducibility issues with some Python packages
|
||||
# See https://nixos.org/manual/nixpkgs/stable/#python-setup.py-bdist_wheel-cannot-create-.whl
|
||||
unset SOURCE_DATE_EPOCH
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -56,6 +56,7 @@ build_flags = -Wno-missing-field-initializers
|
||||
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
||||
-DMESHTASTIC_EXCLUDE_POWERMON=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
|
||||
@@ -119,7 +120,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/37ad715b76cd6ca4aa500a4a4d9740e3cdf3e3cb.zip
|
||||
https://github.com/meshtastic/device-ui/archive/63967a4a557d33d56fc5746f9128200dde2d88c5.zip
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
[environmental_base]
|
||||
|
||||
Submodule protobufs updated: 77c8329a59...bc63a57f9e
12
shell.nix
Normal file
12
shell.nix
Normal file
@@ -0,0 +1,12 @@
|
||||
(import (
|
||||
let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
nodeName = lock.nodes.root.inputs.flake-compat;
|
||||
in
|
||||
fetchTarball {
|
||||
url =
|
||||
lock.nodes.${nodeName}.locked.url
|
||||
or "https://github.com/NixOS/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.${nodeName}.locked.narHash;
|
||||
}
|
||||
) { src = ./.; }).shellNix
|
||||
@@ -816,6 +816,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)
|
||||
|
||||
@@ -390,9 +390,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
|
||||
@@ -428,12 +425,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -431,45 +431,6 @@ static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool i
|
||||
return iconTop + tallest - 1;
|
||||
}
|
||||
|
||||
static void drawRoundedRectOutline(OLEDDisplay *display, int x, int y, int w, int h, int r)
|
||||
{
|
||||
if (w <= 1 || h <= 1)
|
||||
return;
|
||||
|
||||
if (r < 0)
|
||||
r = 0;
|
||||
|
||||
int maxR = (std::min(w, h) / 2) - 1;
|
||||
if (r > maxR)
|
||||
r = maxR;
|
||||
|
||||
if (r == 0) {
|
||||
display->drawRect(x, y, w, h);
|
||||
return;
|
||||
}
|
||||
|
||||
const int x0 = x;
|
||||
const int y0 = y;
|
||||
const int x1 = x + w - 1;
|
||||
const int y1 = y + h - 1;
|
||||
|
||||
// sides
|
||||
if (x0 + r <= x1 - r) {
|
||||
display->drawLine(x0 + r, y0, x1 - r, y0); // top
|
||||
display->drawLine(x0 + r, y1, x1 - r, y1); // bottom
|
||||
}
|
||||
if (y0 + r <= y1 - r) {
|
||||
display->drawLine(x0, y0 + r, x0, y1 - r); // left
|
||||
display->drawLine(x1, y0 + r, x1, y1 - r); // right
|
||||
}
|
||||
|
||||
// corner arcs
|
||||
display->drawCircleQuads(x0 + r, y0 + r, r, 2); // top left
|
||||
display->drawCircleQuads(x1 - r, y0 + r, r, 1); // top right
|
||||
display->drawCircleQuads(x1 - r, y1 - r, r, 8); // bottom right
|
||||
display->drawCircleQuads(x0 + r, y1 - r, r, 4); // bottom left
|
||||
}
|
||||
|
||||
static std::vector<MessageBlock> buildMessageBlocks(const std::vector<bool> &isHeaderVec, const std::vector<bool> &isMineVec)
|
||||
{
|
||||
std::vector<MessageBlock> blocks;
|
||||
@@ -909,27 +870,37 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
bubbleW = std::max(1, rightEdge - bubbleX);
|
||||
|
||||
if (bubbleW > 1 && bubbleH > 1) {
|
||||
int r = BUBBLE_RADIUS;
|
||||
int maxR = (std::min(bubbleW, bubbleH) / 2) - 1;
|
||||
if (maxR < 0)
|
||||
maxR = 0;
|
||||
if (r > maxR)
|
||||
r = maxR;
|
||||
|
||||
drawRoundedRectOutline(display, bubbleX, topY, bubbleW, bubbleH, r);
|
||||
const int extra = 3;
|
||||
const int rr = r + extra;
|
||||
int x1 = bubbleX + bubbleW - 1;
|
||||
int y1 = topY + bubbleH - 1;
|
||||
|
||||
if (!b.mine) {
|
||||
// top-left corner square
|
||||
display->drawLine(bubbleX, topY, bubbleX + rr, topY);
|
||||
display->drawLine(bubbleX, topY, bubbleX, topY + rr);
|
||||
if (b.mine) {
|
||||
// Send Message (Right side)
|
||||
display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH);
|
||||
// Top Right Corner
|
||||
display->drawRect(x1, topY, 2, 1);
|
||||
display->drawRect(x1, topY, 1, 2);
|
||||
// Bottom Right Corner
|
||||
display->drawRect(x1 - 1, bottomY - 2, 2, 1);
|
||||
display->drawRect(x1, bottomY - 3, 1, 2);
|
||||
// Knock the corners off to make a bubble
|
||||
display->setColor(BLACK);
|
||||
display->drawRect(x1 - bubbleW, topY - 1, 1, 1);
|
||||
display->drawRect(x1 - bubbleW, bottomY - 1, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
} else {
|
||||
// bottom-right corner square
|
||||
display->drawLine(x1 - rr, y1, x1, y1);
|
||||
display->drawLine(x1, y1 - rr, x1, y1);
|
||||
// Received Message (Left Side)
|
||||
display->drawRect(bubbleX, topY, bubbleW + 1, bubbleH);
|
||||
// Top Left Corner
|
||||
display->drawRect(bubbleX + 1, topY + 1, 2, 1);
|
||||
display->drawRect(bubbleX + 1, topY + 1, 1, 2);
|
||||
// Bottom Left Corner
|
||||
display->drawRect(bubbleX + 1, bottomY - 1, 2, 1);
|
||||
display->drawRect(bubbleX + 1, bottomY - 2, 1, 2);
|
||||
// Knock the corners off to make a bubble
|
||||
display->setColor(BLACK);
|
||||
display->drawRect(bubbleX + bubbleW, topY, 1, 1);
|
||||
display->drawRect(bubbleX + bubbleW, bottomY, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,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 +65,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 +116,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 +148,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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,7 +46,7 @@ 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
|
||||
|
||||
@@ -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
|
||||
@@ -176,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,7 +21,7 @@ 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;
|
||||
|
||||
@@ -19,10 +19,10 @@ namespace NicheGraphics::InkHUD
|
||||
enum MenuAction {
|
||||
NO_ACTION,
|
||||
SEND_PING,
|
||||
FREE_TEXT,
|
||||
STORE_CANNEDMESSAGE_SELECTION,
|
||||
SEND_CANNEDMESSAGE,
|
||||
SHUTDOWN,
|
||||
BACK,
|
||||
NEXT_TILE,
|
||||
TOGGLE_BACKLIGHT,
|
||||
TOGGLE_GPS,
|
||||
|
||||
@@ -90,6 +90,8 @@ void InkHUD::MenuApplet::onForeground()
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
OSThread::enabled = true;
|
||||
|
||||
freeTextMode = false;
|
||||
|
||||
// Upgrade the refresh to FAST, for guaranteed responsiveness
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
}
|
||||
@@ -116,6 +118,8 @@ void InkHUD::MenuApplet::onBackground()
|
||||
SystemApplet::lockRequests = false;
|
||||
SystemApplet::handleInput = false;
|
||||
|
||||
handleFreeText = false;
|
||||
|
||||
// Restore the user applet whose tile we borrowed
|
||||
if (borrowedTileOwner)
|
||||
borrowedTileOwner->bringToForeground();
|
||||
@@ -325,10 +329,6 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
}
|
||||
break;
|
||||
|
||||
case BACK:
|
||||
showPage(item.nextPage);
|
||||
return;
|
||||
|
||||
case NEXT_TILE:
|
||||
inkhud->nextTile();
|
||||
// Unselect menu item after tile change
|
||||
@@ -344,12 +344,26 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL);
|
||||
break;
|
||||
|
||||
case FREE_TEXT:
|
||||
OSThread::enabled = false;
|
||||
handleFreeText = true;
|
||||
cm.freeTextItem.rawText.erase(); // clear the previous freetext message
|
||||
freeTextMode = true; // render input field instead of normal menu
|
||||
// Open the on-screen keyboard if the joystick is enabled
|
||||
if (settings->joystick.enabled)
|
||||
inkhud->openKeyboard();
|
||||
break;
|
||||
|
||||
case STORE_CANNEDMESSAGE_SELECTION:
|
||||
cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
|
||||
if (!settings->joystick.enabled)
|
||||
cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
|
||||
else
|
||||
cm.selectedMessageItem = &cm.messageItems.at(cursor - 2); // Minus two: offset for the "Send Ping" and free text entry
|
||||
break;
|
||||
|
||||
case SEND_CANNEDMESSAGE:
|
||||
cm.selectedRecipientItem = &cm.recipientItems.at(cursor);
|
||||
// send selected message
|
||||
sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str());
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here
|
||||
break;
|
||||
@@ -868,6 +882,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
|
||||
switch (page) {
|
||||
case ROOT:
|
||||
previousPage = MenuPage::EXIT;
|
||||
// Optional: next applet
|
||||
if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1)
|
||||
items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown
|
||||
@@ -878,7 +893,6 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
items.push_back(MenuItem("Node Config", MenuPage::NODE_CONFIG));
|
||||
items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN));
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
previousPage = MenuPage::EXIT;
|
||||
break;
|
||||
|
||||
case SEND:
|
||||
@@ -888,11 +902,12 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
|
||||
case CANNEDMESSAGE_RECIPIENT:
|
||||
populateRecipientPage();
|
||||
previousPage = MenuPage::OPTIONS;
|
||||
previousPage = MenuPage::SEND;
|
||||
break;
|
||||
|
||||
case OPTIONS:
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::ROOT));
|
||||
previousPage = MenuPage::ROOT;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
// Optional: backlight
|
||||
if (settings->optionalMenuItems.backlight)
|
||||
items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label
|
||||
@@ -916,31 +931,32 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
invertedColors = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
|
||||
items.push_back(MenuItem("Invert Color", MenuAction::TOGGLE_INVERT_COLOR, MenuPage::OPTIONS, &invertedColors));
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
previousPage = MenuPage::ROOT;
|
||||
break;
|
||||
|
||||
case APPLETS:
|
||||
populateAppletPage(); // must be first
|
||||
items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS));
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
previousPage = MenuPage::OPTIONS;
|
||||
populateAppletPage(); // must be first
|
||||
items.insert(items.begin(), MenuItem("Back", previousPage));
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
break;
|
||||
|
||||
case AUTOSHOW:
|
||||
populateAutoshowPage(); // must be first
|
||||
items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS));
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
previousPage = MenuPage::OPTIONS;
|
||||
populateAutoshowPage(); // must be first
|
||||
items.insert(items.begin(), MenuItem("Back", previousPage));
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
break;
|
||||
|
||||
case RECENTS:
|
||||
previousPage = MenuPage::OPTIONS;
|
||||
populateRecentsPage(); // builds only the options
|
||||
items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS));
|
||||
items.insert(items.begin(), MenuItem("Back", previousPage));
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
break;
|
||||
|
||||
case NODE_CONFIG:
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::ROOT));
|
||||
previousPage = MenuPage::ROOT;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
// Radio Config Section
|
||||
items.push_back(MenuItem::Header("Radio Config"));
|
||||
items.push_back(MenuItem("LoRa", MenuPage::NODE_CONFIG_LORA));
|
||||
@@ -965,8 +981,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
break;
|
||||
|
||||
case NODE_CONFIG_DEVICE: {
|
||||
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
||||
previousPage = MenuPage::NODE_CONFIG;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
|
||||
const char *role = DisplayFormatters::getDeviceRole(config.device.role);
|
||||
nodeConfigLabels.emplace_back("Role: " + std::string(role));
|
||||
@@ -981,7 +997,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
}
|
||||
|
||||
case NODE_CONFIG_POSITION: {
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
||||
previousPage = MenuPage::NODE_CONFIG;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
#if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS
|
||||
const auto mode = config.position.gps_mode;
|
||||
if (mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) {
|
||||
@@ -996,7 +1013,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
}
|
||||
|
||||
case NODE_CONFIG_POWER: {
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
||||
previousPage = MenuPage::NODE_CONFIG;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
#if defined(ARCH_ESP32)
|
||||
items.push_back(MenuItem("Powersave", MenuAction::TOGGLE_POWER_SAVE, MenuPage::EXIT, &config.power.is_power_saving));
|
||||
#endif
|
||||
@@ -1029,7 +1047,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
}
|
||||
|
||||
case NODE_CONFIG_POWER_ADC_CAL: {
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_POWER));
|
||||
previousPage = MenuPage::NODE_CONFIG_POWER;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
|
||||
// Instruction text (header-style, non-selectable)
|
||||
items.push_back(MenuItem::Header("Run on full charge Only"));
|
||||
@@ -1042,7 +1061,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
}
|
||||
|
||||
case NODE_CONFIG_NETWORK: {
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
||||
previousPage = MenuPage::NODE_CONFIG;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
|
||||
const char *wifiLabel = config.network.wifi_enabled ? "WiFi: On" : "WiFi: Off";
|
||||
|
||||
@@ -1099,7 +1119,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
}
|
||||
|
||||
case NODE_CONFIG_DISPLAY: {
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
||||
previousPage = MenuPage::NODE_CONFIG;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
|
||||
items.push_back(MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::NODE_CONFIG_DISPLAY,
|
||||
&config.display.use_12h_clock));
|
||||
@@ -1114,7 +1135,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
}
|
||||
|
||||
case NODE_CONFIG_BLUETOOTH: {
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
||||
previousPage = MenuPage::NODE_CONFIG;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
|
||||
const char *btLabel = config.bluetooth.enabled ? "Bluetooth: On" : "Bluetooth: Off";
|
||||
items.push_back(MenuItem(btLabel, MenuAction::TOGGLE_BLUETOOTH, MenuPage::EXIT));
|
||||
@@ -1127,8 +1149,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
}
|
||||
|
||||
case NODE_CONFIG_LORA: {
|
||||
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
||||
previousPage = MenuPage::NODE_CONFIG;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
|
||||
const char *region = myRegion ? myRegion->name : "Unset";
|
||||
nodeConfigLabels.emplace_back("Region: " + std::string(region));
|
||||
@@ -1150,7 +1172,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
}
|
||||
|
||||
case NODE_CONFIG_CHANNELS: {
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
||||
previousPage = MenuPage::NODE_CONFIG;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
|
||||
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
|
||||
meshtastic_Channel &ch = channels.getByIndex(i);
|
||||
@@ -1181,7 +1204,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
}
|
||||
|
||||
case NODE_CONFIG_CHANNEL_DETAIL: {
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_CHANNELS));
|
||||
previousPage = MenuPage::NODE_CONFIG_CHANNELS;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
|
||||
meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
|
||||
|
||||
@@ -1226,7 +1250,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
}
|
||||
|
||||
case NODE_CONFIG_CHANNEL_PRECISION: {
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_CHANNEL_DETAIL));
|
||||
previousPage = MenuPage::NODE_CONFIG_CHANNEL_DETAIL;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
|
||||
if (!ch.settings.has_module_settings || ch.settings.module_settings.position_precision == 0) {
|
||||
items.push_back(MenuItem("Position is Off", MenuPage::NODE_CONFIG_CHANNEL_DETAIL));
|
||||
@@ -1247,7 +1272,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
}
|
||||
|
||||
case NODE_CONFIG_DEVICE_ROLE: {
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_DEVICE));
|
||||
previousPage = MenuPage::NODE_CONFIG_DEVICE;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
items.push_back(MenuItem("Client", MenuAction::SET_ROLE_CLIENT, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Client Mute", MenuAction::SET_ROLE_CLIENT_MUTE, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Router", MenuAction::SET_ROLE_ROUTER, MenuPage::EXIT));
|
||||
@@ -1257,7 +1283,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
}
|
||||
|
||||
case TIMEZONE:
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_DEVICE));
|
||||
previousPage = MenuPage::NODE_CONFIG_DEVICE;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
items.push_back(MenuItem("US/Hawaii", SET_TZ_US_HAWAII, MenuPage::NODE_CONFIG_DEVICE));
|
||||
items.push_back(MenuItem("US/Alaska", SET_TZ_US_ALASKA, MenuPage::NODE_CONFIG_DEVICE));
|
||||
items.push_back(MenuItem("US/Pacific", SET_TZ_US_PACIFIC, MenuPage::NODE_CONFIG_DEVICE));
|
||||
@@ -1279,7 +1306,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
break;
|
||||
|
||||
case REGION:
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_LORA));
|
||||
previousPage = MenuPage::NODE_CONFIG_LORA;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
items.push_back(MenuItem("US", MenuAction::SET_REGION_US, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("EU 868", MenuAction::SET_REGION_EU_868, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("EU 433", MenuAction::SET_REGION_EU_433, MenuPage::EXIT));
|
||||
@@ -1310,7 +1338,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
break;
|
||||
|
||||
case NODE_CONFIG_PRESET: {
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_LORA));
|
||||
previousPage = MenuPage::NODE_CONFIG_LORA;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
items.push_back(MenuItem("Long Moderate", MenuAction::SET_PRESET_LONG_MODERATE, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Long Fast", MenuAction::SET_PRESET_LONG_FAST, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Medium Slow", MenuAction::SET_PRESET_MEDIUM_SLOW, MenuPage::EXIT));
|
||||
@@ -1323,7 +1352,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
}
|
||||
// Administration Section
|
||||
case NODE_CONFIG_ADMIN_RESET:
|
||||
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
|
||||
previousPage = MenuPage::NODE_CONFIG;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
items.push_back(MenuItem("Reset All", MenuAction::RESET_NODEDB_ALL, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Keep Favorites Only", MenuAction::RESET_NODEDB_KEEP_FAVORITES, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
@@ -1361,8 +1391,14 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
currentPage = page;
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onRender()
|
||||
void InkHUD::MenuApplet::onRender(bool full)
|
||||
{
|
||||
// Free text mode draws a text input field and skips the normal rendering
|
||||
if (freeTextMode) {
|
||||
drawInputField(0, fontSmall.lineHeight(), X(1.0), Y(1.0) - fontSmall.lineHeight() - 1, cm.freeTextItem.rawText);
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.size() == 0)
|
||||
LOG_ERROR("Empty Menu");
|
||||
|
||||
@@ -1481,44 +1517,48 @@ void InkHUD::MenuApplet::onRender()
|
||||
|
||||
void InkHUD::MenuApplet::onButtonShortPress()
|
||||
{
|
||||
// Push the auto-close timer back
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
if (!freeTextMode) {
|
||||
// Push the auto-close timer back
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
if (!settings->joystick.enabled) {
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
cursor = 0;
|
||||
} else {
|
||||
do {
|
||||
cursor = (cursor + 1) % items.size();
|
||||
} while (items.at(cursor).isHeader);
|
||||
}
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
} else {
|
||||
if (cursorShown)
|
||||
execute(items.at(cursor));
|
||||
else
|
||||
showPage(MenuPage::EXIT);
|
||||
if (!wantsToRender())
|
||||
if (!settings->joystick.enabled) {
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
cursor = 0;
|
||||
} else {
|
||||
do {
|
||||
cursor = (cursor + 1) % items.size();
|
||||
} while (items.at(cursor).isHeader);
|
||||
}
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
} else {
|
||||
if (cursorShown)
|
||||
execute(items.at(cursor));
|
||||
else
|
||||
showPage(MenuPage::EXIT);
|
||||
if (!wantsToRender())
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onButtonLongPress()
|
||||
{
|
||||
// Push the auto-close timer back
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
if (!freeTextMode) {
|
||||
// Push the auto-close timer back
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
if (cursorShown)
|
||||
execute(items.at(cursor));
|
||||
else
|
||||
showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close
|
||||
if (cursorShown)
|
||||
execute(items.at(cursor));
|
||||
else
|
||||
showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close
|
||||
|
||||
// If we didn't already request a specialized update, when handling a menu action,
|
||||
// then perform the usual fast update.
|
||||
// FAST keeps things responsive: important because we're dealing with user input
|
||||
if (!wantsToRender())
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
// If we didn't already request a specialized update, when handling a menu action,
|
||||
// then perform the usual fast update.
|
||||
// FAST keeps things responsive: important because we're dealing with user input
|
||||
if (!wantsToRender())
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onExitShort()
|
||||
@@ -1531,56 +1571,107 @@ void InkHUD::MenuApplet::onExitShort()
|
||||
|
||||
void InkHUD::MenuApplet::onNavUp()
|
||||
{
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
if (!freeTextMode) {
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
cursor = 0;
|
||||
} else {
|
||||
do {
|
||||
if (cursor == 0)
|
||||
cursor = items.size() - 1;
|
||||
else
|
||||
cursor--;
|
||||
} while (items.at(cursor).isHeader);
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
cursor = 0;
|
||||
} else {
|
||||
do {
|
||||
if (cursor == 0)
|
||||
cursor = items.size() - 1;
|
||||
else
|
||||
cursor--;
|
||||
} while (items.at(cursor).isHeader);
|
||||
}
|
||||
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onNavDown()
|
||||
{
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
if (!freeTextMode) {
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
cursor = 0;
|
||||
} else {
|
||||
do {
|
||||
cursor = (cursor + 1) % items.size();
|
||||
} while (items.at(cursor).isHeader);
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
cursor = 0;
|
||||
} else {
|
||||
do {
|
||||
cursor = (cursor + 1) % items.size();
|
||||
} while (items.at(cursor).isHeader);
|
||||
}
|
||||
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onNavLeft()
|
||||
{
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
if (!freeTextMode) {
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
// Go to the previous menu page
|
||||
showPage(previousPage);
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
// Go to the previous menu page
|
||||
showPage(previousPage);
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onNavRight()
|
||||
{
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
if (!freeTextMode) {
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
if (cursorShown)
|
||||
execute(items.at(cursor));
|
||||
if (!wantsToRender())
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
}
|
||||
|
||||
if (cursorShown)
|
||||
execute(items.at(cursor));
|
||||
if (!wantsToRender())
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
void InkHUD::MenuApplet::onFreeText(char c)
|
||||
{
|
||||
if (cm.freeTextItem.rawText.length() >= menuTextLimit && c != '\b')
|
||||
return;
|
||||
if (c == '\b') {
|
||||
if (!cm.freeTextItem.rawText.empty())
|
||||
cm.freeTextItem.rawText.pop_back();
|
||||
} else {
|
||||
cm.freeTextItem.rawText += c;
|
||||
}
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onFreeTextDone()
|
||||
{
|
||||
// Restart the auto-close timeout
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
OSThread::enabled = true;
|
||||
|
||||
handleFreeText = false;
|
||||
freeTextMode = false;
|
||||
|
||||
if (!cm.freeTextItem.rawText.empty()) {
|
||||
cm.selectedMessageItem = &cm.freeTextItem;
|
||||
showPage(MenuPage::CANNEDMESSAGE_RECIPIENT);
|
||||
}
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onFreeTextCancel()
|
||||
{
|
||||
// Restart the auto-close timeout
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
OSThread::enabled = true;
|
||||
|
||||
handleFreeText = false;
|
||||
freeTextMode = false;
|
||||
|
||||
// Clear the free text message
|
||||
cm.freeTextItem.rawText.erase();
|
||||
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
// Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu
|
||||
@@ -1635,6 +1726,10 @@ void InkHUD::MenuApplet::populateSendPage()
|
||||
// Position / NodeInfo packet
|
||||
items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT));
|
||||
|
||||
// If joystick is available, include the Free Text option
|
||||
if (settings->joystick.enabled)
|
||||
items.push_back(MenuItem("Free Text", MenuAction::FREE_TEXT, MenuPage::SEND));
|
||||
|
||||
// One menu item for each canned message
|
||||
uint8_t count = cm.store->size();
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
@@ -1734,6 +1829,48 @@ void InkHUD::MenuApplet::populateRecipientPage()
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, std::string text)
|
||||
{
|
||||
setFont(fontSmall);
|
||||
uint16_t wrapMaxH = 0;
|
||||
|
||||
// Draw the text, input box, and cursor
|
||||
// Adjusting the box for screen height
|
||||
while (wrapMaxH < height - fontSmall.lineHeight()) {
|
||||
wrapMaxH += fontSmall.lineHeight();
|
||||
}
|
||||
|
||||
// If the text is so long that it goes outside of the input box, the text is actually rendered off screen.
|
||||
uint32_t textHeight = getWrappedTextHeight(0, width - 5, text);
|
||||
if (!text.empty()) {
|
||||
uint16_t textPadding = X(1.0) > Y(1.0) ? wrapMaxH - textHeight : wrapMaxH - textHeight + 1;
|
||||
if (textHeight > wrapMaxH)
|
||||
printWrapped(2, textPadding, width - 5, text);
|
||||
else
|
||||
printWrapped(2, top + 2, width - 5, text);
|
||||
}
|
||||
|
||||
uint16_t textCursorX = text.empty() ? 1 : getCursorX();
|
||||
uint16_t textCursorY = text.empty() ? fontSmall.lineHeight() + 2 : getCursorY() - fontSmall.lineHeight() + 3;
|
||||
|
||||
if (textCursorX + 1 > width - 5) {
|
||||
textCursorX = getCursorX() - width + 5;
|
||||
textCursorY += fontSmall.lineHeight();
|
||||
}
|
||||
|
||||
fillRect(textCursorX + 1, textCursorY, 1, fontSmall.lineHeight(), BLACK);
|
||||
|
||||
// A white rectangle clears the top part of the screen for any text that's printed beyond the input box
|
||||
fillRect(0, 0, X(1.0), top, WHITE);
|
||||
|
||||
// Draw character limit
|
||||
std::string ftlen = std::to_string(text.length()) + "/" + to_string(menuTextLimit);
|
||||
uint16_t textLen = getTextWidth(ftlen);
|
||||
printAt(X(1.0) - textLen - 2, 0, ftlen);
|
||||
|
||||
// Draw the border
|
||||
drawRect(0, top, width, wrapMaxH + 5, BLACK);
|
||||
}
|
||||
// Renders the panel shown at the top of the root menu.
|
||||
// Displays the clock, and several other pieces of instantaneous system info,
|
||||
// which we'd prefer not to have displayed in a normal applet, as they update too frequently.
|
||||
@@ -1875,4 +2012,4 @@ void InkHUD::MenuApplet::freeCannedMessageResources()
|
||||
cm.messageItems.clear();
|
||||
cm.recipientItems.clear();
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_INKHUD
|
||||
#endif // MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
@@ -32,7 +32,10 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
void onNavDown() override;
|
||||
void onNavLeft() override;
|
||||
void onNavRight() override;
|
||||
void onRender() override;
|
||||
void onFreeText(char c) override;
|
||||
void onFreeTextDone() override;
|
||||
void onFreeTextCancel() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
void show(Tile *t); // Open the menu, onto a user tile
|
||||
void setStartPage(MenuPage page);
|
||||
@@ -51,6 +54,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
|
||||
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
||||
|
||||
void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height,
|
||||
std::string text); // Draw input field for free text
|
||||
uint16_t getSystemInfoPanelHeight();
|
||||
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
|
||||
uint16_t *height = nullptr); // Info panel at top of root menu
|
||||
@@ -62,8 +67,9 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
MenuPage previousPage = MenuPage::EXIT;
|
||||
uint8_t cursor = 0; // Which menu item is currently highlighted
|
||||
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
|
||||
|
||||
bool freeTextMode = false;
|
||||
uint16_t systemInfoPanelHeight = 0; // Need to know before we render
|
||||
uint16_t menuTextLimit = 200;
|
||||
|
||||
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
|
||||
std::vector<std::string> nodeConfigLabels; // Persistent labels for Node Config pages
|
||||
@@ -104,6 +110,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
// Cleared onBackground (when MenuApplet closes)
|
||||
std::vector<MessageItem> messageItems;
|
||||
std::vector<RecipientItem> recipientItems;
|
||||
|
||||
MessageItem freeTextItem;
|
||||
} cm;
|
||||
|
||||
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
|
||||
|
||||
@@ -65,7 +65,7 @@ int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket
|
||||
return 0;
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onRender()
|
||||
void InkHUD::NotificationApplet::onRender(bool full)
|
||||
{
|
||||
// Clear the region beneath the tile
|
||||
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
||||
@@ -139,54 +139,47 @@ void InkHUD::NotificationApplet::onForeground()
|
||||
void InkHUD::NotificationApplet::onBackground()
|
||||
{
|
||||
handleInput = false;
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onButtonShortPress()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onButtonLongPress()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onExitShort()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onExitLong()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onNavUp()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onNavDown()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onNavLeft()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onNavRight()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
// Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification
|
||||
|
||||
@@ -26,7 +26,7 @@ class NotificationApplet : public SystemApplet
|
||||
public:
|
||||
NotificationApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonShortPress() override;
|
||||
|
||||
@@ -9,7 +9,7 @@ InkHUD::PairingApplet::PairingApplet()
|
||||
bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus);
|
||||
}
|
||||
|
||||
void InkHUD::PairingApplet::onRender()
|
||||
void InkHUD::PairingApplet::onRender(bool full)
|
||||
{
|
||||
// Header
|
||||
setFont(fontMedium);
|
||||
@@ -45,7 +45,7 @@ void InkHUD::PairingApplet::onBackground()
|
||||
|
||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
|
||||
}
|
||||
|
||||
int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status)
|
||||
|
||||
@@ -22,7 +22,7 @@ class PairingApplet : public SystemApplet
|
||||
public:
|
||||
PairingApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::PlaceholderApplet::onRender()
|
||||
void InkHUD::PlaceholderApplet::onRender(bool full)
|
||||
{
|
||||
// This placeholder applet fills its area with sparse diagonal lines
|
||||
hatchRegion(0, 0, width(), height(), 8, BLACK);
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace NicheGraphics::InkHUD
|
||||
class PlaceholderApplet : public SystemApplet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
// Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet.
|
||||
// The window manager decides when and where it should be rendered
|
||||
|
||||
@@ -45,7 +45,7 @@ InkHUD::TipsApplet::TipsApplet()
|
||||
bringToForeground();
|
||||
}
|
||||
|
||||
void InkHUD::TipsApplet::onRender()
|
||||
void InkHUD::TipsApplet::onRender(bool full)
|
||||
{
|
||||
switch (tipQueue.front()) {
|
||||
case Tip::WELCOME:
|
||||
@@ -261,7 +261,7 @@ void InkHUD::TipsApplet::onBackground()
|
||||
|
||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
|
||||
}
|
||||
|
||||
// While our SystemApplet::handleInput flag is true
|
||||
@@ -292,9 +292,8 @@ void InkHUD::TipsApplet::onButtonShortPress()
|
||||
inkhud->persistence->saveSettings();
|
||||
}
|
||||
|
||||
// Close applet and clean the screen
|
||||
// Close applet
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
} else {
|
||||
requestUpdate();
|
||||
}
|
||||
@@ -306,4 +305,4 @@ void InkHUD::TipsApplet::onExitShort()
|
||||
onButtonShortPress();
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -33,7 +33,7 @@ class TipsApplet : public SystemApplet
|
||||
public:
|
||||
TipsApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonShortPress() override;
|
||||
|
||||
@@ -34,7 +34,7 @@ int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *
|
||||
return 0;
|
||||
}
|
||||
|
||||
void InkHUD::AllMessageApplet::onRender()
|
||||
void InkHUD::AllMessageApplet::onRender(bool full)
|
||||
{
|
||||
// Find newest message, regardless of whether DM or broadcast
|
||||
MessageStore::Message *message;
|
||||
|
||||
@@ -30,7 +30,7 @@ class Applet;
|
||||
class AllMessageApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
|
||||
@@ -37,7 +37,7 @@ int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
||||
return 0;
|
||||
}
|
||||
|
||||
void InkHUD::DMApplet::onRender()
|
||||
void InkHUD::DMApplet::onRender(bool full)
|
||||
{
|
||||
// Abort if no text message
|
||||
if (!latestMessage->dm.sender) {
|
||||
|
||||
@@ -30,7 +30,7 @@ class Applet;
|
||||
class DMApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::PositionsApplet::onRender()
|
||||
void InkHUD::PositionsApplet::onRender(bool full)
|
||||
{
|
||||
// Draw the usual map applet first
|
||||
MapApplet::onRender();
|
||||
MapApplet::onRender(full);
|
||||
|
||||
// Draw our latest "node of interest" as a special marker
|
||||
// -------------------------------------------------------
|
||||
|
||||
@@ -24,7 +24,7 @@ class PositionsApplet : public MapApplet, public SinglePortModule
|
||||
{
|
||||
public:
|
||||
PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {}
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
protected:
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
@@ -22,7 +22,7 @@ InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex)
|
||||
store = new MessageStore("ch" + to_string(channelIndex));
|
||||
}
|
||||
|
||||
void InkHUD::ThreadedMessageApplet::onRender()
|
||||
void InkHUD::ThreadedMessageApplet::onRender(bool full)
|
||||
{
|
||||
// =============
|
||||
// Draw a header
|
||||
|
||||
@@ -36,7 +36,7 @@ class ThreadedMessageApplet : public Applet, public SinglePortModule
|
||||
explicit ThreadedMessageApplet(uint8_t channelIndex);
|
||||
ThreadedMessageApplet() = delete;
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
|
||||
@@ -238,6 +238,39 @@ void InkHUD::Events::onNavRight()
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onFreeText(char c)
|
||||
{
|
||||
// Trigger the first system applet that wants to handle the new character
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleFreeText) {
|
||||
sa->onFreeText(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onFreeTextDone()
|
||||
{
|
||||
// Trigger the first system applet that wants to handle it
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleFreeText) {
|
||||
sa->onFreeTextDone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onFreeTextCancel()
|
||||
{
|
||||
// Trigger the first system applet that wants to handle it
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleFreeText) {
|
||||
sa->onFreeTextCancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Callback for deepSleepObserver
|
||||
// Returns 0 to signal that we agree to sleep now
|
||||
int InkHUD::Events::beforeDeepSleep(void *unused)
|
||||
@@ -266,7 +299,7 @@ int InkHUD::Events::beforeDeepSleep(void *unused)
|
||||
// then prepared a final powered-off screen for us, which shows device shortname.
|
||||
// We're updating to show that one now.
|
||||
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false);
|
||||
delay(1000); // Cooldown, before potentially yanking display power
|
||||
|
||||
// InkHUD shutdown complete
|
||||
|
||||
@@ -37,6 +37,11 @@ class Events
|
||||
void onNavLeft(); // Navigate left
|
||||
void onNavRight(); // Navigate right
|
||||
|
||||
// Free text typing events
|
||||
void onFreeText(char c); // New freetext character input
|
||||
void onFreeTextDone();
|
||||
void onFreeTextCancel();
|
||||
|
||||
int beforeDeepSleep(void *unused); // Prepare for shutdown
|
||||
int beforeReboot(void *unused); // Prepare for reboot
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message
|
||||
|
||||
@@ -175,6 +175,25 @@ void InkHUD::InkHUD::navRight()
|
||||
}
|
||||
}
|
||||
|
||||
// Call this for keyboard input
|
||||
// The Keyboard Applet also calls this
|
||||
void InkHUD::InkHUD::freeText(char c)
|
||||
{
|
||||
events->onFreeText(c);
|
||||
}
|
||||
|
||||
// Call this to complete a freetext input
|
||||
void InkHUD::InkHUD::freeTextDone()
|
||||
{
|
||||
events->onFreeTextDone();
|
||||
}
|
||||
|
||||
// Call this to cancel a freetext input
|
||||
void InkHUD::InkHUD::freeTextCancel()
|
||||
{
|
||||
events->onFreeTextCancel();
|
||||
}
|
||||
|
||||
// Cycle the next user applet to the foreground
|
||||
// Only activated applets are cycled
|
||||
// If user has a multi-applet layout, the applets will cycle on the "focused tile"
|
||||
@@ -204,6 +223,18 @@ void InkHUD::InkHUD::openAlignStick()
|
||||
windowManager->openAlignStick();
|
||||
}
|
||||
|
||||
// Open the on-screen keyboard
|
||||
void InkHUD::InkHUD::openKeyboard()
|
||||
{
|
||||
windowManager->openKeyboard();
|
||||
}
|
||||
|
||||
// Close the on-screen keyboard
|
||||
void InkHUD::InkHUD::closeKeyboard()
|
||||
{
|
||||
windowManager->closeKeyboard();
|
||||
}
|
||||
|
||||
// In layouts where multiple applets are shown at once, change which tile is focused
|
||||
// The focused tile in the one which cycles applets on button short press, and displays menu on long press
|
||||
void InkHUD::InkHUD::nextTile()
|
||||
@@ -252,10 +283,11 @@ void InkHUD::InkHUD::requestUpdate()
|
||||
// Ignores all diplomacy:
|
||||
// - the display *will* update
|
||||
// - the specified update type *will* be used
|
||||
// If the all parameter is true, the whole screen buffer is cleared and re-rendered
|
||||
// If the async parameter is false, code flow is blocked while the update takes place
|
||||
void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool async)
|
||||
void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool all, bool async)
|
||||
{
|
||||
renderer->forceUpdate(type, async);
|
||||
renderer->forceUpdate(type, all, async);
|
||||
}
|
||||
|
||||
// Wait for any in-progress display update to complete before continuing
|
||||
|
||||
@@ -63,6 +63,11 @@ class InkHUD
|
||||
void navLeft();
|
||||
void navRight();
|
||||
|
||||
// Freetext handlers
|
||||
void freeText(char c);
|
||||
void freeTextDone();
|
||||
void freeTextCancel();
|
||||
|
||||
// Trigger UI changes
|
||||
// - called by various InkHUD components
|
||||
// - suitable(?) for use by aux button, connected in variant nicheGraphics.h
|
||||
@@ -71,6 +76,8 @@ class InkHUD
|
||||
void prevApplet();
|
||||
void openMenu();
|
||||
void openAlignStick();
|
||||
void openKeyboard();
|
||||
void closeKeyboard();
|
||||
void nextTile();
|
||||
void prevTile();
|
||||
void rotate();
|
||||
@@ -84,7 +91,8 @@ class InkHUD
|
||||
// - called by various InkHUD components
|
||||
|
||||
void requestUpdate();
|
||||
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool async = true);
|
||||
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool all = false,
|
||||
bool async = true);
|
||||
void awaitUpdate();
|
||||
|
||||
// (Re)configuring WindowManager
|
||||
|
||||
@@ -56,15 +56,16 @@ void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMul
|
||||
|
||||
void InkHUD::Renderer::begin()
|
||||
{
|
||||
forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
|
||||
forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false);
|
||||
}
|
||||
|
||||
// Set a flag, which will be picked up by runOnce, ASAP.
|
||||
// Quite likely, multiple applets will all want to respond to one event (Observable, etc)
|
||||
// Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next runOnce
|
||||
void InkHUD::Renderer::requestUpdate()
|
||||
void InkHUD::Renderer::requestUpdate(bool all)
|
||||
{
|
||||
requested = true;
|
||||
renderAll |= all;
|
||||
|
||||
// We will run the thread as soon as we loop(),
|
||||
// after all Applets have had a chance to observe whatever event set this off
|
||||
@@ -79,10 +80,11 @@ void InkHUD::Renderer::requestUpdate()
|
||||
// Sometimes, however, we will want to trigger a display update manually, in the absence of any sort of applet event
|
||||
// Display health, for example.
|
||||
// In these situations, we use forceUpdate
|
||||
void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool async)
|
||||
void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool all, bool async)
|
||||
{
|
||||
requested = true;
|
||||
forced = true;
|
||||
renderAll |= all;
|
||||
displayHealth.forceUpdateType(type);
|
||||
|
||||
// Normally, we need to start the timer, in case the display is busy and we briefly defer the update
|
||||
@@ -219,7 +221,8 @@ void InkHUD::Renderer::render(bool async)
|
||||
Drivers::EInk::UpdateTypes updateType = decideUpdateType();
|
||||
|
||||
// Render the new image
|
||||
clearBuffer();
|
||||
if (renderAll)
|
||||
clearBuffer();
|
||||
renderUserApplets();
|
||||
renderPlaceholders();
|
||||
renderSystemApplets();
|
||||
@@ -247,6 +250,7 @@ void InkHUD::Renderer::render(bool async)
|
||||
// Tidy up, ready for a new request
|
||||
requested = false;
|
||||
forced = false;
|
||||
renderAll = false;
|
||||
}
|
||||
|
||||
// Manually fill the image buffer with WHITE
|
||||
@@ -259,6 +263,76 @@ void InkHUD::Renderer::clearBuffer()
|
||||
memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth);
|
||||
}
|
||||
|
||||
// Manually clear the pixels below a tile
|
||||
void InkHUD::Renderer::clearTile(Tile *t)
|
||||
{
|
||||
// Rotate the tile dimensions
|
||||
int16_t left = 0;
|
||||
int16_t top = 0;
|
||||
uint16_t width = 0;
|
||||
uint16_t height = 0;
|
||||
switch (settings->rotation) {
|
||||
case 0:
|
||||
left = t->getLeft();
|
||||
top = t->getTop();
|
||||
width = t->getWidth();
|
||||
height = t->getHeight();
|
||||
break;
|
||||
case 1:
|
||||
left = driver->width - (t->getTop() + t->getHeight());
|
||||
top = t->getLeft();
|
||||
width = t->getHeight();
|
||||
height = t->getWidth();
|
||||
break;
|
||||
case 2:
|
||||
left = driver->width - (t->getLeft() + t->getWidth());
|
||||
top = driver->height - (t->getTop() + t->getHeight());
|
||||
width = t->getWidth();
|
||||
height = t->getHeight();
|
||||
break;
|
||||
case 3:
|
||||
left = t->getTop();
|
||||
top = driver->height - (t->getLeft() + t->getWidth());
|
||||
width = t->getHeight();
|
||||
height = t->getWidth();
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate the bounds to clear
|
||||
uint16_t xStart = (left < 0) ? 0 : left;
|
||||
uint16_t yStart = (top < 0) ? 0 : top;
|
||||
if (xStart >= driver->width || yStart >= driver->height || left + width < 0 || top + height < 0)
|
||||
return; // the box is completely off the screen
|
||||
uint16_t xEnd = left + width;
|
||||
uint16_t yEnd = top + height;
|
||||
if (xEnd > driver->width)
|
||||
xEnd = driver->width;
|
||||
if (yEnd > driver->height)
|
||||
yEnd = driver->height;
|
||||
|
||||
// Clear the pixels
|
||||
if (xStart == 0 && xEnd == driver->width) { // full width box is easier to clear
|
||||
memset(imageBuffer + (yStart * imageBufferWidth), 0xFF, (yEnd - yStart) * imageBufferWidth);
|
||||
} else {
|
||||
const uint16_t byteStart = (xStart / 8) + 1;
|
||||
const uint16_t byteEnd = xEnd / 8;
|
||||
const uint8_t leadingByte = 0xFF >> (xStart - ((byteStart - 1) * 8));
|
||||
const uint8_t trailingByte = (0xFF00 >> (xEnd - (byteEnd * 8))) & 0xFF;
|
||||
for (uint16_t i = yStart * imageBufferWidth; i < yEnd * imageBufferWidth; i += imageBufferWidth) {
|
||||
// Set the leading byte
|
||||
imageBuffer[i + byteStart - 1] |= leadingByte;
|
||||
|
||||
// Set the continuous bytes
|
||||
if (byteStart < byteEnd)
|
||||
memset(imageBuffer + i + byteStart, 0xFF, byteEnd - byteStart);
|
||||
|
||||
// Set the trailing byte
|
||||
if (byteEnd != imageBufferWidth)
|
||||
imageBuffer[i + byteEnd] |= trailingByte;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Renderer::checkLocks()
|
||||
{
|
||||
lockRendering = nullptr;
|
||||
@@ -323,12 +397,12 @@ Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType()
|
||||
if (!forced) {
|
||||
// User applets
|
||||
for (Applet *ua : inkhud->userApplets) {
|
||||
if (ua && ua->isForeground())
|
||||
if (ua && ua->isForeground() && (ua->wantsToRender() || renderAll))
|
||||
displayHealth.requestUpdateType(ua->wantsUpdateType());
|
||||
}
|
||||
// System Applets
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa && sa->isForeground())
|
||||
if (sa && sa->isForeground() && (sa->wantsToRender() || sa->alwaysRender || renderAll))
|
||||
displayHealth.requestUpdateType(sa->wantsUpdateType());
|
||||
}
|
||||
}
|
||||
@@ -346,9 +420,16 @@ void InkHUD::Renderer::renderUserApplets()
|
||||
|
||||
// Render any user applets which are currently visible
|
||||
for (Applet *ua : inkhud->userApplets) {
|
||||
if (ua && ua->isActive() && ua->isForeground()) {
|
||||
if (ua && ua->isActive() && ua->isForeground() && (ua->wantsToRender() || renderAll)) {
|
||||
|
||||
// Clear the tile unless the applet wants to draw over its previous render
|
||||
// or everything is getting re-rendered anyways
|
||||
if (ua->wantsFullRender() && !renderAll)
|
||||
clearTile(ua->getTile());
|
||||
|
||||
uint32_t start = millis();
|
||||
ua->render(); // Draw!
|
||||
bool full = ua->wantsFullRender() || renderAll;
|
||||
ua->render(full); // Draw!
|
||||
uint32_t stop = millis();
|
||||
LOG_DEBUG("%s took %dms to render", ua->name, stop - start);
|
||||
}
|
||||
@@ -370,6 +451,9 @@ void InkHUD::Renderer::renderSystemApplets()
|
||||
if (!sa->isForeground())
|
||||
continue;
|
||||
|
||||
if (!sa->wantsToRender() && !sa->alwaysRender && !renderAll)
|
||||
continue;
|
||||
|
||||
// Skip if locked by another applet
|
||||
if (lockRendering && lockRendering != sa)
|
||||
continue;
|
||||
@@ -381,8 +465,14 @@ void InkHUD::Renderer::renderSystemApplets()
|
||||
|
||||
assert(sa->getTile());
|
||||
|
||||
// Clear the tile unless the applet wants to draw over its previous render
|
||||
// or everything is getting re-rendered anyways
|
||||
if (sa->wantsFullRender() && !renderAll)
|
||||
clearTile(sa->getTile());
|
||||
|
||||
// uint32_t start = millis();
|
||||
sa->render(); // Draw!
|
||||
bool full = sa->wantsFullRender() || renderAll;
|
||||
sa->render(full); // Draw!
|
||||
// uint32_t stop = millis();
|
||||
// LOG_DEBUG("%s took %dms to render", sa->name, stop - start);
|
||||
}
|
||||
@@ -409,7 +499,10 @@ void InkHUD::Renderer::renderPlaceholders()
|
||||
// uint32_t start = millis();
|
||||
for (Tile *t : emptyTiles) {
|
||||
t->assignApplet(placeholder);
|
||||
placeholder->render();
|
||||
// Clear the tile unless everything is getting re-rendered
|
||||
if (!renderAll)
|
||||
clearTile(t);
|
||||
placeholder->render(true); // full render
|
||||
t->assignApplet(nullptr);
|
||||
}
|
||||
// uint32_t stop = millis();
|
||||
|
||||
@@ -37,8 +37,8 @@ class Renderer : protected concurrency::OSThread
|
||||
|
||||
// Call these to make the image change
|
||||
|
||||
void requestUpdate(); // Update display, if a foreground applet has info it wants to show
|
||||
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED,
|
||||
void requestUpdate(bool all = false); // Update display, if a foreground applet has info it wants to show
|
||||
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool all = false,
|
||||
bool async = true); // Update display, regardless of whether any applets requested this
|
||||
|
||||
// Wait for an update to complete
|
||||
@@ -65,6 +65,7 @@ class Renderer : protected concurrency::OSThread
|
||||
// Steps of the rendering process
|
||||
|
||||
void clearBuffer();
|
||||
void clearTile(Tile *t);
|
||||
void checkLocks();
|
||||
bool shouldUpdate();
|
||||
Drivers::EInk::UpdateTypes decideUpdateType();
|
||||
@@ -85,6 +86,7 @@ class Renderer : protected concurrency::OSThread
|
||||
|
||||
bool requested = false;
|
||||
bool forced = false;
|
||||
bool renderAll = false;
|
||||
|
||||
// For convenience
|
||||
InkHUD *inkhud = nullptr;
|
||||
|
||||
@@ -22,9 +22,11 @@ class SystemApplet : public Applet
|
||||
public:
|
||||
// System applets have the right to:
|
||||
|
||||
bool handleInput = false; // - respond to input from the user button
|
||||
bool lockRendering = false; // - prevent other applets from being rendered during an update
|
||||
bool lockRequests = false; // - prevent other applets from triggering display updates
|
||||
bool handleInput = false; // - respond to input from the user button
|
||||
bool handleFreeText = false; // - respond to free text input
|
||||
bool lockRendering = false; // - prevent other applets from being rendered during an update
|
||||
bool lockRequests = false; // - prevent other applets from triggering display updates
|
||||
bool alwaysRender = false; // - render every time the screen is updated
|
||||
|
||||
virtual void onReboot() { onShutdown(); } // - handle reboot specially
|
||||
virtual void onApplyingChanges() {}
|
||||
@@ -41,4 +43,4 @@ class SystemApplet : public Applet
|
||||
|
||||
}; // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -18,7 +18,7 @@ static int32_t runtaskHighlight()
|
||||
LOG_DEBUG("Dismissing Highlight");
|
||||
InkHUD::Tile::highlightShown = false;
|
||||
InkHUD::Tile::highlightTarget = nullptr;
|
||||
InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting
|
||||
InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true); // Re-render, clearing the highlighting
|
||||
return taskHighlight->disable();
|
||||
}
|
||||
static void inittaskHighlight()
|
||||
@@ -190,6 +190,18 @@ void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c)
|
||||
}
|
||||
}
|
||||
|
||||
// Used in Renderer for clearing the tile
|
||||
int16_t InkHUD::Tile::getLeft()
|
||||
{
|
||||
return left;
|
||||
}
|
||||
|
||||
// Used in Renderer for clearing the tile
|
||||
int16_t InkHUD::Tile::getTop()
|
||||
{
|
||||
return top;
|
||||
}
|
||||
|
||||
// Called by Applet base class, when setting applet dimensions, immediately before render
|
||||
uint16_t InkHUD::Tile::getWidth()
|
||||
{
|
||||
@@ -220,7 +232,7 @@ void InkHUD::Tile::requestHighlight()
|
||||
{
|
||||
Tile::highlightTarget = this;
|
||||
Tile::highlightShown = false;
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true);
|
||||
}
|
||||
|
||||
// Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first
|
||||
|
||||
@@ -29,6 +29,8 @@ class Tile
|
||||
void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout
|
||||
void setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually
|
||||
void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet
|
||||
int16_t getLeft();
|
||||
int16_t getTop();
|
||||
uint16_t getWidth();
|
||||
uint16_t getHeight();
|
||||
static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "./Applets/System/AlignStick/AlignStickApplet.h"
|
||||
#include "./Applets/System/BatteryIcon/BatteryIconApplet.h"
|
||||
#include "./Applets/System/Keyboard/KeyboardApplet.h"
|
||||
#include "./Applets/System/Logo/LogoApplet.h"
|
||||
#include "./Applets/System/Menu/MenuApplet.h"
|
||||
#include "./Applets/System/Notification/NotificationApplet.h"
|
||||
@@ -148,6 +149,28 @@ void InkHUD::WindowManager::openAlignStick()
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::WindowManager::openKeyboard()
|
||||
{
|
||||
KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard");
|
||||
|
||||
if (keyboard) {
|
||||
keyboard->bringToForeground();
|
||||
keyboardOpen = true;
|
||||
changeLayout();
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::WindowManager::closeKeyboard()
|
||||
{
|
||||
KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard");
|
||||
|
||||
if (keyboard) {
|
||||
keyboard->sendToBackground();
|
||||
keyboardOpen = false;
|
||||
changeLayout();
|
||||
}
|
||||
}
|
||||
|
||||
// On the currently focussed tile: cycle to the next available user applet
|
||||
// Applets available for this must be activated, and not already displayed on another tile
|
||||
void InkHUD::WindowManager::nextApplet()
|
||||
@@ -272,7 +295,6 @@ void InkHUD::WindowManager::toggleBatteryIcon()
|
||||
batteryIcon->sendToBackground();
|
||||
|
||||
// Force-render
|
||||
// - redraw all applets
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
@@ -311,9 +333,25 @@ void InkHUD::WindowManager::changeLayout()
|
||||
menu->show(ft);
|
||||
}
|
||||
|
||||
// Resize for the on-screen keyboard
|
||||
if (keyboardOpen) {
|
||||
// Send all user applets to the background
|
||||
// User applets currently don't handle free text input
|
||||
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++)
|
||||
inkhud->userApplets.at(i)->sendToBackground();
|
||||
// Find the first system applet that can handle freetext and resize it
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleFreeText) {
|
||||
const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight();
|
||||
sa->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height() - keyboardHeight - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force-render
|
||||
// - redraw all applets
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST, true);
|
||||
}
|
||||
|
||||
// Perform necessary reconfiguration when user activates or deactivates applets at run-time
|
||||
@@ -347,7 +385,7 @@ void InkHUD::WindowManager::changeActivatedApplets()
|
||||
|
||||
// Force-render
|
||||
// - redraw all applets
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST, true);
|
||||
}
|
||||
|
||||
// Some applets may be permitted to bring themselves to foreground, to show new data
|
||||
@@ -433,8 +471,10 @@ void InkHUD::WindowManager::createSystemApplets()
|
||||
addSystemApplet("Logo", new LogoApplet, new Tile);
|
||||
addSystemApplet("Pairing", new PairingApplet, new Tile);
|
||||
addSystemApplet("Tips", new TipsApplet, new Tile);
|
||||
if (settings->joystick.enabled)
|
||||
if (settings->joystick.enabled) {
|
||||
addSystemApplet("AlignStick", new AlignStickApplet, new Tile);
|
||||
addSystemApplet("Keyboard", new KeyboardApplet, new Tile);
|
||||
}
|
||||
|
||||
addSystemApplet("Menu", new MenuApplet, nullptr);
|
||||
|
||||
@@ -457,9 +497,13 @@ void InkHUD::WindowManager::placeSystemTiles()
|
||||
inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
||||
inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
||||
inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
||||
if (settings->joystick.enabled)
|
||||
if (settings->joystick.enabled) {
|
||||
inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
||||
|
||||
const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight();
|
||||
inkhud->getSystemApplet("Keyboard")
|
||||
->getTile()
|
||||
->setRegion(0, inkhud->height() - keyboardHeight, inkhud->width(), keyboardHeight);
|
||||
}
|
||||
inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20);
|
||||
|
||||
const uint16_t batteryIconHeight = Applet::getHeaderHeight() - 2 - 2;
|
||||
|
||||
@@ -31,6 +31,8 @@ class WindowManager
|
||||
void prevTile();
|
||||
void openMenu();
|
||||
void openAlignStick();
|
||||
void openKeyboard();
|
||||
void closeKeyboard();
|
||||
void nextApplet();
|
||||
void prevApplet();
|
||||
void rotate();
|
||||
@@ -64,6 +66,7 @@ class WindowManager
|
||||
void findOrphanApplets(); // Find any applets left-behind when layout changes
|
||||
|
||||
std::vector<Tile *> userTiles; // Tiles which can host user applets
|
||||
bool keyboardOpen = false;
|
||||
|
||||
// For convenience
|
||||
InkHUD *inkhud = nullptr;
|
||||
|
||||
@@ -174,7 +174,7 @@ class BasicExampleApplet : public Applet
|
||||
// You must have an onRender() method
|
||||
// All drawing happens here
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -183,7 +183,7 @@ The `onRender` method is called when the display image is redrawn. This can happ
|
||||
```cpp
|
||||
// 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!");
|
||||
}
|
||||
|
||||
@@ -20,20 +20,20 @@ constexpr uint8_t modifierLeftShift = 0b0001;
|
||||
|
||||
// Num chars per key, Modulus for rotating through characters
|
||||
static uint8_t HackadayCommunicatorTapMod[_TCA8418_NUM_KEYS] = {
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 2, 2, 2, 1, 2, 2, 0, 0, 0, 2, 1, 2, 2, 0, 1, 1, 0,
|
||||
};
|
||||
|
||||
static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{},
|
||||
{},
|
||||
{Key::FUNCTION_F1},
|
||||
{'+'},
|
||||
{'9'},
|
||||
{'8'},
|
||||
{'7'},
|
||||
{'2'},
|
||||
{'3'},
|
||||
{'4'},
|
||||
{'5'},
|
||||
{Key::FUNCTION_F2},
|
||||
{Key::FUNCTION_F3},
|
||||
{Key::FUNCTION_F4},
|
||||
{Key::FUNCTION_F5},
|
||||
{Key::ESC},
|
||||
{'q', 'Q'},
|
||||
{'w', 'W'},
|
||||
@@ -141,6 +141,7 @@ void HackadayCommunicatorKeyboard::pressed(uint8_t key)
|
||||
if (state == Init || state == Busy) {
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Key pressed: %u", key);
|
||||
|
||||
if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) {
|
||||
modifierFlag = 0;
|
||||
|
||||
@@ -1,8 +1,59 @@
|
||||
#include "InputBroker.h"
|
||||
#include "PowerFSM.h" // needed for event trigger
|
||||
#include "configuration.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
#include "input/LinuxInputImpl.h"
|
||||
#include "input/SeesawRotary.h"
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
#endif
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
#include "input/ExpressLRSFiveWay.h"
|
||||
#include "input/RotaryEncoderImpl.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/SerialKeyboardImpl.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#include "input/i2cButton.h"
|
||||
#if HAS_TRACKBALL
|
||||
#include "input/TrackballInterruptImpl1.h"
|
||||
#endif
|
||||
|
||||
#include "modules/StatusLEDModule.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C
|
||||
#include "input/cardKbI2cImpl.h"
|
||||
#endif
|
||||
#include "input/kbMatrixImpl.h"
|
||||
#endif
|
||||
|
||||
#if HAS_BUTTON || defined(ARCH_PORTDUINO)
|
||||
#include "input/ButtonThread.h"
|
||||
|
||||
#if defined(BUTTON_PIN_TOUCH)
|
||||
ButtonThread *TouchButtonThread = nullptr;
|
||||
#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN)
|
||||
static bool touchBacklightWasOn = false;
|
||||
static bool touchBacklightActive = false;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO)
|
||||
ButtonThread *UserButtonThread = nullptr;
|
||||
#endif
|
||||
|
||||
#if defined(ALT_BUTTON_PIN)
|
||||
ButtonThread *BackButtonThread = nullptr;
|
||||
#endif
|
||||
|
||||
#if defined(CANCEL_BUTTON_PIN)
|
||||
ButtonThread *CancelButtonThread = nullptr;
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
InputBroker *inputBroker = nullptr;
|
||||
|
||||
InputBroker::InputBroker()
|
||||
@@ -74,3 +125,262 @@ void InputBroker::pollSoonWorker(void *p)
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
#endif
|
||||
|
||||
void InputBroker::Init()
|
||||
{
|
||||
|
||||
#ifdef BUTTON_PIN
|
||||
#ifdef ARCH_ESP32
|
||||
|
||||
#if ESP_ARDUINO_VERSION_MAJOR >= 3
|
||||
#ifdef BUTTON_NEED_PULLUP
|
||||
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP);
|
||||
#else
|
||||
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
|
||||
#endif
|
||||
#else
|
||||
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
|
||||
#ifdef BUTTON_NEED_PULLUP
|
||||
gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN));
|
||||
delay(10);
|
||||
#endif
|
||||
#ifdef BUTTON_NEED_PULLUP2
|
||||
gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2);
|
||||
delay(10);
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// buttons are now inputBroker, so have to come after setupModules
|
||||
#if HAS_BUTTON
|
||||
int pullup_sense = 0;
|
||||
#ifdef INPUT_PULLUP_SENSE
|
||||
// Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did
|
||||
#ifdef BUTTON_SENSE_TYPE
|
||||
pullup_sense = BUTTON_SENSE_TYPE;
|
||||
#else
|
||||
pullup_sense = INPUT_PULLUP_SENSE;
|
||||
#endif
|
||||
#endif
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
|
||||
if (portduino_config.userButtonPin.enabled) {
|
||||
|
||||
LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin);
|
||||
UserButtonThread = new ButtonThread("UserButton");
|
||||
if (screen) {
|
||||
ButtonConfig config;
|
||||
config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin;
|
||||
config.activeLow = true;
|
||||
config.activePullup = true;
|
||||
config.pullupSense = INPUT_PULLUP;
|
||||
config.intRoutine = []() {
|
||||
UserButtonThread->userButton.tick();
|
||||
UserButtonThread->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||
};
|
||||
config.singlePress = INPUT_BROKER_USER_PRESS;
|
||||
config.longPress = INPUT_BROKER_SELECT;
|
||||
UserButtonThread->initButton(config);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef BUTTON_PIN_TOUCH
|
||||
TouchButtonThread = new ButtonThread("BackButton");
|
||||
ButtonConfig touchConfig;
|
||||
touchConfig.pinNumber = BUTTON_PIN_TOUCH;
|
||||
touchConfig.activeLow = true;
|
||||
touchConfig.activePullup = true;
|
||||
touchConfig.pullupSense = pullup_sense;
|
||||
touchConfig.intRoutine = []() {
|
||||
TouchButtonThread->userButton.tick();
|
||||
TouchButtonThread->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||
};
|
||||
touchConfig.singlePress = INPUT_BROKER_NONE;
|
||||
touchConfig.longPress = INPUT_BROKER_BACK;
|
||||
#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN)
|
||||
// On T-Echo Plus the touch pad should only drive the backlight, not UI navigation/sounds
|
||||
touchConfig.longPress = INPUT_BROKER_NONE;
|
||||
touchConfig.suppressLeadUpSound = true;
|
||||
touchConfig.onPress = []() {
|
||||
touchBacklightWasOn = uiconfig.screen_brightness == 1;
|
||||
if (!touchBacklightWasOn) {
|
||||
digitalWrite(PIN_EINK_EN, HIGH);
|
||||
}
|
||||
touchBacklightActive = true;
|
||||
};
|
||||
touchConfig.onRelease = []() {
|
||||
if (touchBacklightActive && !touchBacklightWasOn) {
|
||||
digitalWrite(PIN_EINK_EN, LOW);
|
||||
}
|
||||
touchBacklightActive = false;
|
||||
};
|
||||
#endif
|
||||
TouchButtonThread->initButton(touchConfig);
|
||||
#endif
|
||||
|
||||
#if defined(CANCEL_BUTTON_PIN)
|
||||
// Buttons. Moved here cause we need NodeDB to be initialized
|
||||
CancelButtonThread = new ButtonThread("CancelButton");
|
||||
ButtonConfig cancelConfig;
|
||||
cancelConfig.pinNumber = CANCEL_BUTTON_PIN;
|
||||
cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW;
|
||||
cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP;
|
||||
cancelConfig.pullupSense = pullup_sense;
|
||||
cancelConfig.intRoutine = []() {
|
||||
CancelButtonThread->userButton.tick();
|
||||
CancelButtonThread->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||
};
|
||||
cancelConfig.singlePress = INPUT_BROKER_CANCEL;
|
||||
cancelConfig.longPress = INPUT_BROKER_SHUTDOWN;
|
||||
cancelConfig.longPressTime = 4000;
|
||||
CancelButtonThread->initButton(cancelConfig);
|
||||
#endif
|
||||
|
||||
#if defined(ALT_BUTTON_PIN)
|
||||
// Buttons. Moved here cause we need NodeDB to be initialized
|
||||
BackButtonThread = new ButtonThread("BackButton");
|
||||
ButtonConfig backConfig;
|
||||
backConfig.pinNumber = ALT_BUTTON_PIN;
|
||||
backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW;
|
||||
backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP;
|
||||
backConfig.pullupSense = pullup_sense;
|
||||
backConfig.intRoutine = []() {
|
||||
BackButtonThread->userButton.tick();
|
||||
BackButtonThread->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||
};
|
||||
backConfig.singlePress = INPUT_BROKER_ALT_PRESS;
|
||||
backConfig.longPress = INPUT_BROKER_ALT_LONG;
|
||||
backConfig.longPressTime = 500;
|
||||
BackButtonThread->initButton(backConfig);
|
||||
#endif
|
||||
|
||||
#if defined(BUTTON_PIN)
|
||||
#if defined(USERPREFS_BUTTON_PIN)
|
||||
int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN;
|
||||
#else
|
||||
int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN;
|
||||
#endif
|
||||
#ifndef BUTTON_ACTIVE_LOW
|
||||
#define BUTTON_ACTIVE_LOW true
|
||||
#endif
|
||||
#ifndef BUTTON_ACTIVE_PULLUP
|
||||
#define BUTTON_ACTIVE_PULLUP true
|
||||
#endif
|
||||
|
||||
// Buttons. Moved here cause we need NodeDB to be initialized
|
||||
// If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP
|
||||
UserButtonThread = new ButtonThread("UserButton");
|
||||
if (screen) {
|
||||
ButtonConfig userConfig;
|
||||
userConfig.pinNumber = (uint8_t)_pinNum;
|
||||
userConfig.activeLow = BUTTON_ACTIVE_LOW;
|
||||
userConfig.activePullup = BUTTON_ACTIVE_PULLUP;
|
||||
userConfig.pullupSense = pullup_sense;
|
||||
userConfig.intRoutine = []() {
|
||||
UserButtonThread->userButton.tick();
|
||||
UserButtonThread->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||
};
|
||||
userConfig.singlePress = INPUT_BROKER_USER_PRESS;
|
||||
userConfig.longPress = INPUT_BROKER_SELECT;
|
||||
userConfig.longPressTime = 500;
|
||||
userConfig.longLongPress = INPUT_BROKER_SHUTDOWN;
|
||||
UserButtonThread->initButton(userConfig);
|
||||
} else {
|
||||
ButtonConfig userConfigNoScreen;
|
||||
userConfigNoScreen.pinNumber = (uint8_t)_pinNum;
|
||||
userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW;
|
||||
userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP;
|
||||
userConfigNoScreen.pullupSense = pullup_sense;
|
||||
userConfigNoScreen.intRoutine = []() {
|
||||
UserButtonThread->userButton.tick();
|
||||
UserButtonThread->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||
};
|
||||
userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS;
|
||||
userConfigNoScreen.longPress = INPUT_BROKER_NONE;
|
||||
userConfigNoScreen.longPressTime = 500;
|
||||
userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN;
|
||||
userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING;
|
||||
userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE;
|
||||
UserButtonThread->initButton(userConfigNoScreen);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
#if defined(T_LORA_PAGER)
|
||||
// use a special FSM based rotary encoder version for T-LoRa Pager
|
||||
rotaryEncoderImpl = new RotaryEncoderImpl();
|
||||
if (!rotaryEncoderImpl->init()) {
|
||||
delete rotaryEncoderImpl;
|
||||
rotaryEncoderImpl = nullptr;
|
||||
}
|
||||
#elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2)
|
||||
upDownInterruptImpl1 = new UpDownInterruptImpl1();
|
||||
if (!upDownInterruptImpl1->init()) {
|
||||
delete upDownInterruptImpl1;
|
||||
upDownInterruptImpl1 = nullptr;
|
||||
}
|
||||
#else
|
||||
rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
|
||||
if (!rotaryEncoderInterruptImpl1->init()) {
|
||||
delete rotaryEncoderInterruptImpl1;
|
||||
rotaryEncoderInterruptImpl1 = nullptr;
|
||||
}
|
||||
#endif
|
||||
cardKbI2cImpl = new CardKbI2cImpl();
|
||||
cardKbI2cImpl->init();
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
i2cButton = new i2cButtonThread("i2cButtonThread");
|
||||
#endif
|
||||
#ifdef INPUTBROKER_MATRIX_TYPE
|
||||
kbMatrixImpl = new KbMatrixImpl();
|
||||
kbMatrixImpl->init();
|
||||
#endif // INPUTBROKER_MATRIX_TYPE
|
||||
#ifdef INPUTBROKER_SERIAL_TYPE
|
||||
aSerialKeyboardImpl = new SerialKeyboardImpl();
|
||||
aSerialKeyboardImpl->init();
|
||||
#endif // INPUTBROKER_MATRIX_TYPE
|
||||
}
|
||||
#endif // HAS_BUTTON
|
||||
#if ARCH_PORTDUINO
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") {
|
||||
seesawRotary = new SeesawRotary("SeesawRotary");
|
||||
if (!seesawRotary->init()) {
|
||||
delete seesawRotary;
|
||||
seesawRotary = nullptr;
|
||||
}
|
||||
aLinuxInputImpl = new LinuxInputImpl();
|
||||
aLinuxInputImpl->init();
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
trackballInterruptImpl1 = new TrackballInterruptImpl1();
|
||||
trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS);
|
||||
}
|
||||
#endif
|
||||
#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE
|
||||
expressLRSFiveWayInput = new ExpressLRSFiveWay();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Observer.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "freertosinc.h"
|
||||
|
||||
#ifdef InputBrokerDebug
|
||||
@@ -27,6 +28,11 @@ enum input_broker_event {
|
||||
INPUT_BROKER_SHUTDOWN = 0x9b,
|
||||
INPUT_BROKER_GPS_TOGGLE = 0x9e,
|
||||
INPUT_BROKER_SEND_PING = 0xaf,
|
||||
INPUT_BROKER_FN_F1 = 0xf1,
|
||||
INPUT_BROKER_FN_F2 = 0xf2,
|
||||
INPUT_BROKER_FN_F3 = 0xf3,
|
||||
INPUT_BROKER_FN_F4 = 0xf4,
|
||||
INPUT_BROKER_FN_F5 = 0xf5,
|
||||
INPUT_BROKER_MATRIXKEY = 0xFE,
|
||||
INPUT_BROKER_ANYKEY = 0xff
|
||||
|
||||
@@ -71,6 +77,7 @@ class InputBroker : public Observable<const InputEvent *>
|
||||
void queueInputEvent(const InputEvent *event);
|
||||
void processInputEventQueue();
|
||||
#endif
|
||||
void Init();
|
||||
|
||||
protected:
|
||||
int handleInputEvent(const InputEvent *event);
|
||||
@@ -84,4 +91,5 @@ class InputBroker : public Observable<const InputEvent *>
|
||||
#endif
|
||||
};
|
||||
|
||||
extern InputBroker *inputBroker;
|
||||
extern InputBroker *inputBroker;
|
||||
extern bool runASAP;
|
||||
@@ -5,7 +5,6 @@
|
||||
SerialKeyboard *globalSerialKeyboard = nullptr;
|
||||
|
||||
#ifdef INPUTBROKER_SERIAL_TYPE
|
||||
#define CANNED_MESSAGE_MODULE_ENABLE 1 // in case it's not set in the variant file
|
||||
|
||||
#if INPUTBROKER_SERIAL_TYPE == 1 // It's a Chatter
|
||||
// 3 SHIFT level (lower case, upper case, numbers), up to 4 repeated presses, button number
|
||||
|
||||
@@ -26,7 +26,12 @@ class TCA8418KeyboardBase
|
||||
GPS_TOGGLE = 0x9E,
|
||||
MUTE_TOGGLE = 0xAC,
|
||||
SEND_PING = 0xAF,
|
||||
BL_TOGGLE = 0xAB
|
||||
BL_TOGGLE = 0xAB,
|
||||
FUNCTION_F1 = 0xF1,
|
||||
FUNCTION_F2 = 0xF2,
|
||||
FUNCTION_F3 = 0xF3,
|
||||
FUNCTION_F4 = 0xF4,
|
||||
FUNCTION_F5 = 0xF5
|
||||
};
|
||||
|
||||
typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len);
|
||||
|
||||
@@ -321,6 +321,26 @@ int32_t KbI2cBase::runOnce()
|
||||
e.inputEvent = INPUT_BROKER_ANYKEY;
|
||||
e.kbchar = INPUT_BROKER_MSG_TAB;
|
||||
break;
|
||||
case TCA8418KeyboardBase::FUNCTION_F1:
|
||||
e.inputEvent = INPUT_BROKER_FN_F1;
|
||||
e.kbchar = 0x00;
|
||||
break;
|
||||
case TCA8418KeyboardBase::FUNCTION_F2:
|
||||
e.inputEvent = INPUT_BROKER_FN_F2;
|
||||
e.kbchar = 0x00;
|
||||
break;
|
||||
case TCA8418KeyboardBase::FUNCTION_F3:
|
||||
e.inputEvent = INPUT_BROKER_FN_F3;
|
||||
e.kbchar = 0x00;
|
||||
break;
|
||||
case TCA8418KeyboardBase::FUNCTION_F4:
|
||||
e.inputEvent = INPUT_BROKER_FN_F4;
|
||||
e.kbchar = 0x00;
|
||||
break;
|
||||
case TCA8418KeyboardBase::FUNCTION_F5:
|
||||
e.inputEvent = INPUT_BROKER_FN_F5;
|
||||
e.kbchar = 0x00;
|
||||
break;
|
||||
default:
|
||||
if (nextEvent > 127) {
|
||||
e.inputEvent = INPUT_BROKER_NONE;
|
||||
|
||||
271
src/main.cpp
271
src/main.cpp
@@ -120,31 +120,6 @@ void printPartitionTable()
|
||||
#endif // DEBUG_PARTITION_TABLE
|
||||
#endif // ARCH_ESP32
|
||||
|
||||
#if HAS_BUTTON || defined(ARCH_PORTDUINO)
|
||||
#include "input/ButtonThread.h"
|
||||
|
||||
#if defined(BUTTON_PIN_TOUCH)
|
||||
ButtonThread *TouchButtonThread = nullptr;
|
||||
#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN)
|
||||
static bool touchBacklightWasOn = false;
|
||||
static bool touchBacklightActive = false;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO)
|
||||
ButtonThread *UserButtonThread = nullptr;
|
||||
#endif
|
||||
|
||||
#if defined(ALT_BUTTON_PIN)
|
||||
ButtonThread *BackButtonThread = nullptr;
|
||||
#endif
|
||||
|
||||
#if defined(CANCEL_BUTTON_PIN)
|
||||
ButtonThread *CancelButtonThread = nullptr;
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#include "AmbientLightingThread.h"
|
||||
#include "PowerFSMThread.h"
|
||||
|
||||
@@ -379,9 +354,9 @@ void setup()
|
||||
digitalWrite(LED_POWER, LED_STATE_ON);
|
||||
#endif
|
||||
|
||||
#ifdef USER_LED
|
||||
pinMode(USER_LED, OUTPUT);
|
||||
digitalWrite(USER_LED, HIGH ^ LED_STATE_ON);
|
||||
#ifdef LED_NOTIFICATION
|
||||
pinMode(LED_NOTIFICATION, OUTPUT);
|
||||
digitalWrite(LED_NOTIFICATION, HIGH ^ LED_STATE_ON);
|
||||
#endif
|
||||
|
||||
#ifdef WIFI_LED
|
||||
@@ -509,30 +484,6 @@ void setup()
|
||||
LOG_INFO("Wait for peripherals to stabilize");
|
||||
delay(PERIPHERAL_WARMUP_MS);
|
||||
#endif
|
||||
|
||||
#ifdef BUTTON_PIN
|
||||
#ifdef ARCH_ESP32
|
||||
|
||||
#if ESP_ARDUINO_VERSION_MAJOR >= 3
|
||||
#ifdef BUTTON_NEED_PULLUP
|
||||
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP);
|
||||
#else
|
||||
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
|
||||
#endif
|
||||
#else
|
||||
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
|
||||
#ifdef BUTTON_NEED_PULLUP
|
||||
gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN));
|
||||
delay(10);
|
||||
#endif
|
||||
#ifdef BUTTON_NEED_PULLUP2
|
||||
gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2);
|
||||
delay(10);
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
initSPI();
|
||||
|
||||
OSThread::setup();
|
||||
@@ -999,180 +950,9 @@ void setup()
|
||||
nodeDB->hasWarned = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// buttons are now inputBroker, so have to come after setupModules
|
||||
#if HAS_BUTTON
|
||||
int pullup_sense = 0;
|
||||
#ifdef INPUT_PULLUP_SENSE
|
||||
// Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did
|
||||
#ifdef BUTTON_SENSE_TYPE
|
||||
pullup_sense = BUTTON_SENSE_TYPE;
|
||||
#else
|
||||
pullup_sense = INPUT_PULLUP_SENSE;
|
||||
#endif
|
||||
#endif
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
|
||||
if (portduino_config.userButtonPin.enabled) {
|
||||
|
||||
LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin);
|
||||
UserButtonThread = new ButtonThread("UserButton");
|
||||
if (screen) {
|
||||
ButtonConfig config;
|
||||
config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin;
|
||||
config.activeLow = true;
|
||||
config.activePullup = true;
|
||||
config.pullupSense = INPUT_PULLUP;
|
||||
config.intRoutine = []() {
|
||||
UserButtonThread->userButton.tick();
|
||||
UserButtonThread->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
mainDelay.interruptFromISR(&higherWake);
|
||||
};
|
||||
config.singlePress = INPUT_BROKER_USER_PRESS;
|
||||
config.longPress = INPUT_BROKER_SELECT;
|
||||
UserButtonThread->initButton(config);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef BUTTON_PIN_TOUCH
|
||||
TouchButtonThread = new ButtonThread("BackButton");
|
||||
ButtonConfig touchConfig;
|
||||
touchConfig.pinNumber = BUTTON_PIN_TOUCH;
|
||||
touchConfig.activeLow = true;
|
||||
touchConfig.activePullup = true;
|
||||
touchConfig.pullupSense = pullup_sense;
|
||||
touchConfig.intRoutine = []() {
|
||||
TouchButtonThread->userButton.tick();
|
||||
TouchButtonThread->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
mainDelay.interruptFromISR(&higherWake);
|
||||
};
|
||||
touchConfig.singlePress = INPUT_BROKER_NONE;
|
||||
touchConfig.longPress = INPUT_BROKER_BACK;
|
||||
#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN)
|
||||
// On T-Echo Plus the touch pad should only drive the backlight, not UI navigation/sounds
|
||||
touchConfig.longPress = INPUT_BROKER_NONE;
|
||||
touchConfig.suppressLeadUpSound = true;
|
||||
touchConfig.onPress = []() {
|
||||
touchBacklightWasOn = uiconfig.screen_brightness == 1;
|
||||
if (!touchBacklightWasOn) {
|
||||
digitalWrite(PIN_EINK_EN, HIGH);
|
||||
}
|
||||
touchBacklightActive = true;
|
||||
};
|
||||
touchConfig.onRelease = []() {
|
||||
if (touchBacklightActive && !touchBacklightWasOn) {
|
||||
digitalWrite(PIN_EINK_EN, LOW);
|
||||
}
|
||||
touchBacklightActive = false;
|
||||
};
|
||||
#endif
|
||||
TouchButtonThread->initButton(touchConfig);
|
||||
#endif
|
||||
|
||||
#if defined(CANCEL_BUTTON_PIN)
|
||||
// Buttons. Moved here cause we need NodeDB to be initialized
|
||||
CancelButtonThread = new ButtonThread("CancelButton");
|
||||
ButtonConfig cancelConfig;
|
||||
cancelConfig.pinNumber = CANCEL_BUTTON_PIN;
|
||||
cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW;
|
||||
cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP;
|
||||
cancelConfig.pullupSense = pullup_sense;
|
||||
cancelConfig.intRoutine = []() {
|
||||
CancelButtonThread->userButton.tick();
|
||||
CancelButtonThread->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
mainDelay.interruptFromISR(&higherWake);
|
||||
};
|
||||
cancelConfig.singlePress = INPUT_BROKER_CANCEL;
|
||||
cancelConfig.longPress = INPUT_BROKER_SHUTDOWN;
|
||||
cancelConfig.longPressTime = 4000;
|
||||
CancelButtonThread->initButton(cancelConfig);
|
||||
#endif
|
||||
|
||||
#if defined(ALT_BUTTON_PIN)
|
||||
// Buttons. Moved here cause we need NodeDB to be initialized
|
||||
BackButtonThread = new ButtonThread("BackButton");
|
||||
ButtonConfig backConfig;
|
||||
backConfig.pinNumber = ALT_BUTTON_PIN;
|
||||
backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW;
|
||||
backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP;
|
||||
backConfig.pullupSense = pullup_sense;
|
||||
backConfig.intRoutine = []() {
|
||||
BackButtonThread->userButton.tick();
|
||||
BackButtonThread->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
mainDelay.interruptFromISR(&higherWake);
|
||||
};
|
||||
backConfig.singlePress = INPUT_BROKER_ALT_PRESS;
|
||||
backConfig.longPress = INPUT_BROKER_ALT_LONG;
|
||||
backConfig.longPressTime = 500;
|
||||
BackButtonThread->initButton(backConfig);
|
||||
#endif
|
||||
|
||||
#if defined(BUTTON_PIN)
|
||||
#if defined(USERPREFS_BUTTON_PIN)
|
||||
int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN;
|
||||
#else
|
||||
int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN;
|
||||
#endif
|
||||
#ifndef BUTTON_ACTIVE_LOW
|
||||
#define BUTTON_ACTIVE_LOW true
|
||||
#endif
|
||||
#ifndef BUTTON_ACTIVE_PULLUP
|
||||
#define BUTTON_ACTIVE_PULLUP true
|
||||
#endif
|
||||
|
||||
// Buttons. Moved here cause we need NodeDB to be initialized
|
||||
// If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP
|
||||
UserButtonThread = new ButtonThread("UserButton");
|
||||
if (screen) {
|
||||
ButtonConfig userConfig;
|
||||
userConfig.pinNumber = (uint8_t)_pinNum;
|
||||
userConfig.activeLow = BUTTON_ACTIVE_LOW;
|
||||
userConfig.activePullup = BUTTON_ACTIVE_PULLUP;
|
||||
userConfig.pullupSense = pullup_sense;
|
||||
userConfig.intRoutine = []() {
|
||||
UserButtonThread->userButton.tick();
|
||||
UserButtonThread->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
mainDelay.interruptFromISR(&higherWake);
|
||||
};
|
||||
userConfig.singlePress = INPUT_BROKER_USER_PRESS;
|
||||
userConfig.longPress = INPUT_BROKER_SELECT;
|
||||
userConfig.longPressTime = 500;
|
||||
userConfig.longLongPress = INPUT_BROKER_SHUTDOWN;
|
||||
UserButtonThread->initButton(userConfig);
|
||||
} else {
|
||||
ButtonConfig userConfigNoScreen;
|
||||
userConfigNoScreen.pinNumber = (uint8_t)_pinNum;
|
||||
userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW;
|
||||
userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP;
|
||||
userConfigNoScreen.pullupSense = pullup_sense;
|
||||
userConfigNoScreen.intRoutine = []() {
|
||||
UserButtonThread->userButton.tick();
|
||||
UserButtonThread->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
mainDelay.interruptFromISR(&higherWake);
|
||||
};
|
||||
userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS;
|
||||
userConfigNoScreen.longPress = INPUT_BROKER_NONE;
|
||||
userConfigNoScreen.longPressTime = 500;
|
||||
userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN;
|
||||
userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING;
|
||||
userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE;
|
||||
UserButtonThread->initButton(userConfigNoScreen);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
if (inputBroker)
|
||||
inputBroker->Init();
|
||||
#endif
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -1401,7 +1181,43 @@ void loop()
|
||||
if (inputBroker)
|
||||
inputBroker->processInputEventQueue();
|
||||
#endif
|
||||
#if ARCH_PORTDUINO && HAS_TFT
|
||||
#if ARCH_PORTDUINO
|
||||
if (portduino_config.lora_spi_dev == "ch341" && ch341Hal != nullptr) {
|
||||
ch341Hal->checkError();
|
||||
}
|
||||
if (portduino_status.LoRa_in_error && rebootAtMsec == 0) {
|
||||
LOG_ERROR("LoRa in error detected, attempting to recover");
|
||||
if (rIf != nullptr) {
|
||||
delete rIf;
|
||||
rIf = nullptr;
|
||||
}
|
||||
if (portduino_config.lora_spi_dev == "ch341") {
|
||||
if (ch341Hal != nullptr) {
|
||||
delete ch341Hal;
|
||||
ch341Hal = nullptr;
|
||||
sleep(3);
|
||||
}
|
||||
try {
|
||||
ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid,
|
||||
portduino_config.lora_usb_pid);
|
||||
} catch (std::exception &e) {
|
||||
std::cerr << e.what() << std::endl;
|
||||
std::cerr << "Could not initialize CH341 device!" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
if (initLoRa()) {
|
||||
router->addInterface(rIf);
|
||||
portduino_status.LoRa_in_error = false;
|
||||
} else {
|
||||
LOG_WARN("Reconfigure failed, rebooting");
|
||||
if (screen) {
|
||||
screen->showSimpleBanner("Rebooting...");
|
||||
}
|
||||
rebootAtMsec = millis() + 25;
|
||||
}
|
||||
}
|
||||
#if HAS_TFT
|
||||
if (screen && portduino_config.displayPanel == x11 &&
|
||||
config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
auto dispdev = screen->getDisplayDevice();
|
||||
@@ -1409,6 +1225,7 @@ void loop()
|
||||
static_cast<TFTDisplay *>(dispdev)->sdlLoop();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE
|
||||
messageStoreAutosaveTick();
|
||||
#endif
|
||||
|
||||
@@ -91,10 +91,21 @@ template <typename T> bool LR11x0Interface<T>::init()
|
||||
LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz");
|
||||
#endif
|
||||
|
||||
// Allow extra time for TCXO to stabilize after power-on
|
||||
delay(10);
|
||||
|
||||
int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
||||
|
||||
// Retry if we get SPI command failed - some units need extra TCXO stabilization time
|
||||
if (res == RADIOLIB_ERR_SPI_CMD_FAILED) {
|
||||
LOG_WARN("LR11x0 init failed with %d (SPI_CMD_FAILED), retrying after delay...", res);
|
||||
delay(100);
|
||||
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
||||
}
|
||||
|
||||
// \todo Display actual typename of the adapter, not just `LR11x0`
|
||||
LOG_INFO("LR11x0 init result %d", res);
|
||||
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND)
|
||||
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
|
||||
return false;
|
||||
|
||||
LR11x0VersionInfo_t version;
|
||||
@@ -159,7 +170,7 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
|
||||
err = lora.setCodingRate(cr);
|
||||
err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
|
||||
|
||||
@@ -824,16 +824,10 @@ void NodeDB::installDefaultModuleConfig()
|
||||
moduleConfig.external_notification.output_ms = 500;
|
||||
moduleConfig.external_notification.nag_timeout = 2;
|
||||
#endif
|
||||
#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || \
|
||||
defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M6)
|
||||
// Default to PIN_LED2 for external notification output (LED color depends on device variant)
|
||||
#if defined(LED_NOTIFICATION)
|
||||
moduleConfig.external_notification.enabled = true;
|
||||
moduleConfig.external_notification.output = PIN_LED2;
|
||||
#if defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3)
|
||||
moduleConfig.external_notification.active = false;
|
||||
#else
|
||||
moduleConfig.external_notification.active = true;
|
||||
#endif
|
||||
moduleConfig.external_notification.output = LED_NOTIFICATION;
|
||||
moduleConfig.external_notification.active = LED_STATE_ON;
|
||||
moduleConfig.external_notification.alert_message = true;
|
||||
moduleConfig.external_notification.output_ms = 1000;
|
||||
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
|
||||
@@ -857,15 +851,6 @@ void NodeDB::installDefaultModuleConfig()
|
||||
moduleConfig.external_notification.output_ms = 100;
|
||||
moduleConfig.external_notification.active = true;
|
||||
#endif
|
||||
#ifdef ELECROW_ThinkNode_M1
|
||||
// Default to Elecrow USER_LED (blue)
|
||||
moduleConfig.external_notification.enabled = true;
|
||||
moduleConfig.external_notification.output = USER_LED;
|
||||
moduleConfig.external_notification.active = true;
|
||||
moduleConfig.external_notification.alert_message = true;
|
||||
moduleConfig.external_notification.output_ms = 1000;
|
||||
moduleConfig.external_notification.nag_timeout = 60;
|
||||
#endif
|
||||
#ifdef T_LORA_PAGER
|
||||
moduleConfig.canned_message.updown1_enabled = true;
|
||||
moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A;
|
||||
@@ -2247,7 +2232,10 @@ void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, co
|
||||
|
||||
// Currently portuino is mostly used for simulation. Make sure the user notices something really bad happened
|
||||
#ifdef ARCH_PORTDUINO
|
||||
LOG_ERROR("A critical failure occurred, portduino is exiting");
|
||||
exit(2);
|
||||
LOG_ERROR("A critical failure occurred");
|
||||
// TODO: Determine if other critical errors should also cause an immediate exit
|
||||
if (code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE ||
|
||||
code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE)
|
||||
exit(2);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -177,6 +177,9 @@ bool RF95Interface::init()
|
||||
|
||||
int res = lora->begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength);
|
||||
LOG_INFO("RF95 init result %d", res);
|
||||
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
|
||||
return false;
|
||||
|
||||
LOG_INFO("Frequency set to %f", getFreq());
|
||||
LOG_INFO("Bandwidth set to %f", bw);
|
||||
LOG_INFO("Power output set to %d", power);
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
#include "platform/portduino/USBHal.h"
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_STM32WL>
|
||||
#ifdef ARCH_STM32WL
|
||||
#include "STM32WLE5JCInterface.h"
|
||||
#endif
|
||||
|
||||
|
||||
@@ -17,12 +17,6 @@
|
||||
ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p)
|
||||
{
|
||||
if (p->want_ack) {
|
||||
// If someone asks for acks on broadcast, we need the hop limit to be at least one, so that first node that receives our
|
||||
// message will rebroadcast. But asking for hop_limit 0 in that context means the client app has no preference on hop
|
||||
// counts and we want this message to get through the whole mesh, so use the default.
|
||||
if (p->hop_limit == 0) {
|
||||
p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
}
|
||||
DEBUG_HEAP_BEFORE;
|
||||
auto copy = packetPool.allocCopy(*p);
|
||||
DEBUG_HEAP_AFTER("ReliableRouter::send", copy);
|
||||
|
||||
@@ -266,6 +266,13 @@ ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src)
|
||||
}
|
||||
}
|
||||
|
||||
// If someone asks for acks on broadcast, we need the hop limit to be at least one, so that first node that receives our
|
||||
// message will rebroadcast. But asking for hop_limit 0 in that context means the client app has no preference on hop
|
||||
// counts and we want this message to get through the whole mesh, so use the default.
|
||||
if (src == RX_SRC_USER && p->want_ack && p->hop_limit == 0) {
|
||||
p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
}
|
||||
|
||||
return send(p);
|
||||
}
|
||||
}
|
||||
@@ -613,15 +620,19 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
!(p->pki_encrypted != true && (strcasecmp(channels.getName(chIndex), Channels::serialChannel) == 0 ||
|
||||
strcasecmp(channels.getName(chIndex), Channels::gpioChannel) == 0)) &&
|
||||
// Check for valid keys and single node destination
|
||||
config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr &&
|
||||
// Check for a known public key for the destination
|
||||
(node->user.public_key.size == 32) &&
|
||||
config.security.private_key.size == 32 && !isBroadcast(p->to) &&
|
||||
// Some portnums either make no sense to send with PKC
|
||||
p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP &&
|
||||
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) {
|
||||
LOG_DEBUG("Use PKI!");
|
||||
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
|
||||
return meshtastic_Routing_Error_TOO_LARGE;
|
||||
// Check for a known public key for the destination
|
||||
if (node == nullptr || node->user.public_key.size != 32) {
|
||||
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
|
||||
p->decoded.portnum);
|
||||
return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY;
|
||||
}
|
||||
if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) &&
|
||||
memcmp(p->public_key.bytes, node->user.public_key.bytes, 32) != 0) {
|
||||
LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes,
|
||||
|
||||
@@ -269,8 +269,12 @@ template <typename T> void SX126xInterface<T>::setStandby()
|
||||
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
LOG_DEBUG("SX126x standby %s%d", radioLibErr, err);
|
||||
#ifdef ARCH_PORTDUINO
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
portduino_status.LoRa_in_error = true;
|
||||
#else
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
|
||||
#endif
|
||||
isReceiving = false; // If we were receiving, not any more
|
||||
activeReceiveStart = 0;
|
||||
disableInterrupt();
|
||||
@@ -313,7 +317,12 @@ template <typename T> void SX126xInterface<T>::startReceive()
|
||||
int err = lora.startReceiveDutyCycleAuto(preambleLength, 8, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS);
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
LOG_ERROR("SX126X startReceiveDutyCycleAuto %s%d", radioLibErr, err);
|
||||
#ifdef ARCH_PORTDUINO
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
portduino_status.LoRa_in_error = true;
|
||||
#else
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
#endif
|
||||
|
||||
RadioLibInterface::startReceive();
|
||||
|
||||
@@ -341,7 +350,12 @@ template <typename T> bool SX126xInterface<T>::isChannelActive()
|
||||
return true;
|
||||
if (result != RADIOLIB_CHANNEL_FREE)
|
||||
LOG_ERROR("SX126X scanChannel %s%d", radioLibErr, result);
|
||||
#ifdef ARCH_PORTDUINO
|
||||
if (result == RADIOLIB_ERR_WRONG_MODEM)
|
||||
portduino_status.LoRa_in_error = true;
|
||||
#else
|
||||
assert(result != RADIOLIB_ERR_WRONG_MODEM);
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ template <typename T> bool SX128xInterface<T>::init()
|
||||
int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength);
|
||||
// \todo Display actual typename of the adapter, not just `SX128x`
|
||||
LOG_INFO("SX128x init result %d", res);
|
||||
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
|
||||
return false;
|
||||
|
||||
if ((config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && (res == RADIOLIB_ERR_INVALID_FREQUENCY)) {
|
||||
LOG_WARN("Radio only supports 2.4GHz LoRa. Adjusting Region and rebooting");
|
||||
@@ -124,7 +126,7 @@ template <typename T> bool SX128xInterface<T>::reconfigure()
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
|
||||
err = lora.setCodingRate(cr);
|
||||
err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
|
||||
|
||||
@@ -27,6 +27,15 @@ PB_BIND(meshtastic_SharedContact, meshtastic_SharedContact, AUTO)
|
||||
PB_BIND(meshtastic_KeyVerificationAdmin, meshtastic_KeyVerificationAdmin, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_SensorConfig, meshtastic_SensorConfig, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_SCD4X_config, meshtastic_SCD4X_config, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_SEN5X_config, meshtastic_SEN5X_config, AUTO)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -171,6 +171,48 @@ typedef struct _meshtastic_KeyVerificationAdmin {
|
||||
uint32_t security_number;
|
||||
} meshtastic_KeyVerificationAdmin;
|
||||
|
||||
typedef struct _meshtastic_SCD4X_config {
|
||||
/* Set Automatic self-calibration enabled */
|
||||
bool has_set_asc;
|
||||
bool set_asc;
|
||||
/* Recalibration target CO2 concentration in ppm (FRC or ASC) */
|
||||
bool has_set_target_co2_conc;
|
||||
uint32_t set_target_co2_conc;
|
||||
/* Reference temperature in degC */
|
||||
bool has_set_temperature;
|
||||
float set_temperature;
|
||||
/* Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure) */
|
||||
bool has_set_altitude;
|
||||
uint32_t set_altitude;
|
||||
/* Sensor ambient pressure in Pa. 70000 - 120000 Pa (overrides altitude) */
|
||||
bool has_set_ambient_pressure;
|
||||
uint32_t set_ambient_pressure;
|
||||
/* Perform a factory reset of the sensor */
|
||||
bool has_factory_reset;
|
||||
bool factory_reset;
|
||||
/* Power mode for sensor (true for low power, false for normal) */
|
||||
bool has_set_power_mode;
|
||||
bool set_power_mode;
|
||||
} meshtastic_SCD4X_config;
|
||||
|
||||
typedef struct _meshtastic_SEN5X_config {
|
||||
/* Reference temperature in degC */
|
||||
bool has_set_temperature;
|
||||
float set_temperature;
|
||||
/* One-shot mode (true for low power - one-shot mode, false for normal - continuous mode) */
|
||||
bool has_set_one_shot_mode;
|
||||
bool set_one_shot_mode;
|
||||
} meshtastic_SEN5X_config;
|
||||
|
||||
typedef struct _meshtastic_SensorConfig {
|
||||
/* SCD4X CO2 Sensor configuration */
|
||||
bool has_scd4x_config;
|
||||
meshtastic_SCD4X_config scd4x_config;
|
||||
/* SEN5X PM Sensor configuration */
|
||||
bool has_sen5x_config;
|
||||
meshtastic_SEN5X_config sen5x_config;
|
||||
} meshtastic_SensorConfig;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(8) meshtastic_AdminMessage_session_passkey_t;
|
||||
/* This message is handled by the Admin module and is responsible for all settings/channel read/write operations.
|
||||
This message is used to do settings operations to both remote AND local nodes.
|
||||
@@ -303,6 +345,8 @@ typedef struct _meshtastic_AdminMessage {
|
||||
bool nodedb_reset;
|
||||
/* Tell the node to reset into the OTA Loader */
|
||||
meshtastic_AdminMessage_OTAEvent ota_request;
|
||||
/* Parameters and sensor configuration */
|
||||
meshtastic_SensorConfig sensor_config;
|
||||
};
|
||||
/* The node generates this key and sends it with any get_x_response packets.
|
||||
The client MUST include the same key with any set_x commands. Key expires after 300 seconds.
|
||||
@@ -351,6 +395,9 @@ extern "C" {
|
||||
#define meshtastic_KeyVerificationAdmin_message_type_ENUMTYPE meshtastic_KeyVerificationAdmin_MessageType
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Initializer values for message structs */
|
||||
#define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}}
|
||||
#define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0}
|
||||
@@ -359,6 +406,9 @@ extern "C" {
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
|
||||
#define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0, 0}
|
||||
#define meshtastic_KeyVerificationAdmin_init_default {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0}
|
||||
#define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default}
|
||||
#define meshtastic_SCD4X_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_SEN5X_config_init_default {false, 0, false, 0}
|
||||
#define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}}
|
||||
#define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0}
|
||||
#define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {0, {0}}}
|
||||
@@ -366,6 +416,9 @@ extern "C" {
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
|
||||
#define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0, 0}
|
||||
#define meshtastic_KeyVerificationAdmin_init_zero {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0}
|
||||
#define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero}
|
||||
#define meshtastic_SCD4X_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_SEN5X_config_init_zero {false, 0, false, 0}
|
||||
|
||||
/* Field tags (for use in manual encoding/decoding) */
|
||||
#define meshtastic_AdminMessage_InputEvent_event_code_tag 1
|
||||
@@ -387,6 +440,17 @@ extern "C" {
|
||||
#define meshtastic_KeyVerificationAdmin_remote_nodenum_tag 2
|
||||
#define meshtastic_KeyVerificationAdmin_nonce_tag 3
|
||||
#define meshtastic_KeyVerificationAdmin_security_number_tag 4
|
||||
#define meshtastic_SCD4X_config_set_asc_tag 1
|
||||
#define meshtastic_SCD4X_config_set_target_co2_conc_tag 2
|
||||
#define meshtastic_SCD4X_config_set_temperature_tag 3
|
||||
#define meshtastic_SCD4X_config_set_altitude_tag 4
|
||||
#define meshtastic_SCD4X_config_set_ambient_pressure_tag 5
|
||||
#define meshtastic_SCD4X_config_factory_reset_tag 6
|
||||
#define meshtastic_SCD4X_config_set_power_mode_tag 7
|
||||
#define meshtastic_SEN5X_config_set_temperature_tag 1
|
||||
#define meshtastic_SEN5X_config_set_one_shot_mode_tag 2
|
||||
#define meshtastic_SensorConfig_scd4x_config_tag 1
|
||||
#define meshtastic_SensorConfig_sen5x_config_tag 2
|
||||
#define meshtastic_AdminMessage_get_channel_request_tag 1
|
||||
#define meshtastic_AdminMessage_get_channel_response_tag 2
|
||||
#define meshtastic_AdminMessage_get_owner_request_tag 3
|
||||
@@ -443,6 +507,7 @@ extern "C" {
|
||||
#define meshtastic_AdminMessage_factory_reset_config_tag 99
|
||||
#define meshtastic_AdminMessage_nodedb_reset_tag 100
|
||||
#define meshtastic_AdminMessage_ota_request_tag 102
|
||||
#define meshtastic_AdminMessage_sensor_config_tag 103
|
||||
#define meshtastic_AdminMessage_session_passkey_tag 101
|
||||
|
||||
/* Struct field encoding specification for nanopb */
|
||||
@@ -503,7 +568,8 @@ X(a, STATIC, ONEOF, INT32, (payload_variant,shutdown_seconds,shutdown_se
|
||||
X(a, STATIC, ONEOF, INT32, (payload_variant,factory_reset_config,factory_reset_config), 99) \
|
||||
X(a, STATIC, ONEOF, BOOL, (payload_variant,nodedb_reset,nodedb_reset), 100) \
|
||||
X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ota_request,ota_request), 102)
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ota_request,ota_request), 102) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,sensor_config,sensor_config), 103)
|
||||
#define meshtastic_AdminMessage_CALLBACK NULL
|
||||
#define meshtastic_AdminMessage_DEFAULT NULL
|
||||
#define meshtastic_AdminMessage_payload_variant_get_channel_response_MSGTYPE meshtastic_Channel
|
||||
@@ -525,6 +591,7 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ota_request,ota_request), 10
|
||||
#define meshtastic_AdminMessage_payload_variant_add_contact_MSGTYPE meshtastic_SharedContact
|
||||
#define meshtastic_AdminMessage_payload_variant_key_verification_MSGTYPE meshtastic_KeyVerificationAdmin
|
||||
#define meshtastic_AdminMessage_payload_variant_ota_request_MSGTYPE meshtastic_AdminMessage_OTAEvent
|
||||
#define meshtastic_AdminMessage_payload_variant_sensor_config_MSGTYPE meshtastic_SensorConfig
|
||||
|
||||
#define meshtastic_AdminMessage_InputEvent_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, event_code, 1) \
|
||||
@@ -571,6 +638,31 @@ X(a, STATIC, OPTIONAL, UINT32, security_number, 4)
|
||||
#define meshtastic_KeyVerificationAdmin_CALLBACK NULL
|
||||
#define meshtastic_KeyVerificationAdmin_DEFAULT NULL
|
||||
|
||||
#define meshtastic_SensorConfig_FIELDLIST(X, a) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, scd4x_config, 1) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, sen5x_config, 2)
|
||||
#define meshtastic_SensorConfig_CALLBACK NULL
|
||||
#define meshtastic_SensorConfig_DEFAULT NULL
|
||||
#define meshtastic_SensorConfig_scd4x_config_MSGTYPE meshtastic_SCD4X_config
|
||||
#define meshtastic_SensorConfig_sen5x_config_MSGTYPE meshtastic_SEN5X_config
|
||||
|
||||
#define meshtastic_SCD4X_config_FIELDLIST(X, a) \
|
||||
X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \
|
||||
X(a, STATIC, OPTIONAL, UINT32, set_target_co2_conc, 2) \
|
||||
X(a, STATIC, OPTIONAL, FLOAT, set_temperature, 3) \
|
||||
X(a, STATIC, OPTIONAL, UINT32, set_altitude, 4) \
|
||||
X(a, STATIC, OPTIONAL, UINT32, set_ambient_pressure, 5) \
|
||||
X(a, STATIC, OPTIONAL, BOOL, factory_reset, 6) \
|
||||
X(a, STATIC, OPTIONAL, BOOL, set_power_mode, 7)
|
||||
#define meshtastic_SCD4X_config_CALLBACK NULL
|
||||
#define meshtastic_SCD4X_config_DEFAULT NULL
|
||||
|
||||
#define meshtastic_SEN5X_config_FIELDLIST(X, a) \
|
||||
X(a, STATIC, OPTIONAL, FLOAT, set_temperature, 1) \
|
||||
X(a, STATIC, OPTIONAL, BOOL, set_one_shot_mode, 2)
|
||||
#define meshtastic_SEN5X_config_CALLBACK NULL
|
||||
#define meshtastic_SEN5X_config_DEFAULT NULL
|
||||
|
||||
extern const pb_msgdesc_t meshtastic_AdminMessage_msg;
|
||||
extern const pb_msgdesc_t meshtastic_AdminMessage_InputEvent_msg;
|
||||
extern const pb_msgdesc_t meshtastic_AdminMessage_OTAEvent_msg;
|
||||
@@ -578,6 +670,9 @@ extern const pb_msgdesc_t meshtastic_HamParameters_msg;
|
||||
extern const pb_msgdesc_t meshtastic_NodeRemoteHardwarePinsResponse_msg;
|
||||
extern const pb_msgdesc_t meshtastic_SharedContact_msg;
|
||||
extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg;
|
||||
extern const pb_msgdesc_t meshtastic_SensorConfig_msg;
|
||||
extern const pb_msgdesc_t meshtastic_SCD4X_config_msg;
|
||||
extern const pb_msgdesc_t meshtastic_SEN5X_config_msg;
|
||||
|
||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
||||
#define meshtastic_AdminMessage_fields &meshtastic_AdminMessage_msg
|
||||
@@ -587,6 +682,9 @@ extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg;
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_fields &meshtastic_NodeRemoteHardwarePinsResponse_msg
|
||||
#define meshtastic_SharedContact_fields &meshtastic_SharedContact_msg
|
||||
#define meshtastic_KeyVerificationAdmin_fields &meshtastic_KeyVerificationAdmin_msg
|
||||
#define meshtastic_SensorConfig_fields &meshtastic_SensorConfig_msg
|
||||
#define meshtastic_SCD4X_config_fields &meshtastic_SCD4X_config_msg
|
||||
#define meshtastic_SEN5X_config_fields &meshtastic_SEN5X_config_msg
|
||||
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
#define MESHTASTIC_MESHTASTIC_ADMIN_PB_H_MAX_SIZE meshtastic_AdminMessage_size
|
||||
@@ -596,6 +694,9 @@ extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg;
|
||||
#define meshtastic_HamParameters_size 31
|
||||
#define meshtastic_KeyVerificationAdmin_size 25
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_size 496
|
||||
#define meshtastic_SCD4X_config_size 29
|
||||
#define meshtastic_SEN5X_config_size 7
|
||||
#define meshtastic_SensorConfig_size 40
|
||||
#define meshtastic_SharedContact_size 127
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -33,6 +33,9 @@ PB_BIND(meshtastic_Telemetry, meshtastic_Telemetry, 2)
|
||||
PB_BIND(meshtastic_Nau7802Config, meshtastic_Nau7802Config, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_SEN5XState, meshtastic_SEN5XState, AUTO)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -435,6 +435,25 @@ typedef struct _meshtastic_Nau7802Config {
|
||||
float calibrationFactor;
|
||||
} meshtastic_Nau7802Config;
|
||||
|
||||
/* SEN5X State, for saving to flash */
|
||||
typedef struct _meshtastic_SEN5XState {
|
||||
/* Last cleaning time for SEN5X */
|
||||
uint32_t last_cleaning_time;
|
||||
/* Last cleaning time for SEN5X - valid flag */
|
||||
bool last_cleaning_valid;
|
||||
/* Config flag for one-shot mode (see admin.proto) */
|
||||
bool one_shot_mode;
|
||||
/* Last VOC state time for SEN55 */
|
||||
bool has_voc_state_time;
|
||||
uint32_t voc_state_time;
|
||||
/* Last VOC state validity flag for SEN55 */
|
||||
bool has_voc_state_valid;
|
||||
bool voc_state_valid;
|
||||
/* VOC state array (8x uint8t) for SEN55 */
|
||||
bool has_voc_state_array;
|
||||
uint64_t voc_state_array;
|
||||
} meshtastic_SEN5XState;
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -455,6 +474,7 @@ extern "C" {
|
||||
|
||||
|
||||
|
||||
|
||||
/* Initializer values for message structs */
|
||||
#define meshtastic_DeviceMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_EnvironmentMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
@@ -465,6 +485,7 @@ extern "C" {
|
||||
#define meshtastic_HostMetrics_init_default {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""}
|
||||
#define meshtastic_Telemetry_init_default {0, 0, {meshtastic_DeviceMetrics_init_default}}
|
||||
#define meshtastic_Nau7802Config_init_default {0, 0}
|
||||
#define meshtastic_SEN5XState_init_default {0, 0, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_DeviceMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_EnvironmentMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_PowerMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
@@ -474,6 +495,7 @@ extern "C" {
|
||||
#define meshtastic_HostMetrics_init_zero {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""}
|
||||
#define meshtastic_Telemetry_init_zero {0, 0, {meshtastic_DeviceMetrics_init_zero}}
|
||||
#define meshtastic_Nau7802Config_init_zero {0, 0}
|
||||
#define meshtastic_SEN5XState_init_zero {0, 0, 0, false, 0, false, 0, false, 0}
|
||||
|
||||
/* Field tags (for use in manual encoding/decoding) */
|
||||
#define meshtastic_DeviceMetrics_battery_level_tag 1
|
||||
@@ -581,6 +603,12 @@ extern "C" {
|
||||
#define meshtastic_Telemetry_host_metrics_tag 8
|
||||
#define meshtastic_Nau7802Config_zeroOffset_tag 1
|
||||
#define meshtastic_Nau7802Config_calibrationFactor_tag 2
|
||||
#define meshtastic_SEN5XState_last_cleaning_time_tag 1
|
||||
#define meshtastic_SEN5XState_last_cleaning_valid_tag 2
|
||||
#define meshtastic_SEN5XState_one_shot_mode_tag 3
|
||||
#define meshtastic_SEN5XState_voc_state_time_tag 4
|
||||
#define meshtastic_SEN5XState_voc_state_valid_tag 5
|
||||
#define meshtastic_SEN5XState_voc_state_array_tag 6
|
||||
|
||||
/* Struct field encoding specification for nanopb */
|
||||
#define meshtastic_DeviceMetrics_FIELDLIST(X, a) \
|
||||
@@ -731,6 +759,16 @@ X(a, STATIC, SINGULAR, FLOAT, calibrationFactor, 2)
|
||||
#define meshtastic_Nau7802Config_CALLBACK NULL
|
||||
#define meshtastic_Nau7802Config_DEFAULT NULL
|
||||
|
||||
#define meshtastic_SEN5XState_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, last_cleaning_time, 1) \
|
||||
X(a, STATIC, SINGULAR, BOOL, last_cleaning_valid, 2) \
|
||||
X(a, STATIC, SINGULAR, BOOL, one_shot_mode, 3) \
|
||||
X(a, STATIC, OPTIONAL, UINT32, voc_state_time, 4) \
|
||||
X(a, STATIC, OPTIONAL, BOOL, voc_state_valid, 5) \
|
||||
X(a, STATIC, OPTIONAL, FIXED64, voc_state_array, 6)
|
||||
#define meshtastic_SEN5XState_CALLBACK NULL
|
||||
#define meshtastic_SEN5XState_DEFAULT NULL
|
||||
|
||||
extern const pb_msgdesc_t meshtastic_DeviceMetrics_msg;
|
||||
extern const pb_msgdesc_t meshtastic_EnvironmentMetrics_msg;
|
||||
extern const pb_msgdesc_t meshtastic_PowerMetrics_msg;
|
||||
@@ -740,6 +778,7 @@ extern const pb_msgdesc_t meshtastic_HealthMetrics_msg;
|
||||
extern const pb_msgdesc_t meshtastic_HostMetrics_msg;
|
||||
extern const pb_msgdesc_t meshtastic_Telemetry_msg;
|
||||
extern const pb_msgdesc_t meshtastic_Nau7802Config_msg;
|
||||
extern const pb_msgdesc_t meshtastic_SEN5XState_msg;
|
||||
|
||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
||||
#define meshtastic_DeviceMetrics_fields &meshtastic_DeviceMetrics_msg
|
||||
@@ -751,6 +790,7 @@ extern const pb_msgdesc_t meshtastic_Nau7802Config_msg;
|
||||
#define meshtastic_HostMetrics_fields &meshtastic_HostMetrics_msg
|
||||
#define meshtastic_Telemetry_fields &meshtastic_Telemetry_msg
|
||||
#define meshtastic_Nau7802Config_fields &meshtastic_Nau7802Config_msg
|
||||
#define meshtastic_SEN5XState_fields &meshtastic_SEN5XState_msg
|
||||
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
#define MESHTASTIC_MESHTASTIC_TELEMETRY_PB_H_MAX_SIZE meshtastic_Telemetry_size
|
||||
@@ -762,6 +802,7 @@ extern const pb_msgdesc_t meshtastic_Nau7802Config_msg;
|
||||
#define meshtastic_LocalStats_size 87
|
||||
#define meshtastic_Nau7802Config_size 16
|
||||
#define meshtastic_PowerMetrics_size 81
|
||||
#define meshtastic_SEN5XState_size 27
|
||||
#define meshtastic_Telemetry_size 272
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -130,8 +130,7 @@ CannedMessageModule::CannedMessageModule()
|
||||
: SinglePortModule("canned", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("CannedMessage")
|
||||
{
|
||||
this->loadProtoForModule();
|
||||
if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE &&
|
||||
!CANNED_MESSAGE_MODULE_ENABLE) {
|
||||
if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE) {
|
||||
LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled");
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED;
|
||||
disable();
|
||||
|
||||
@@ -27,10 +27,6 @@ enum CannedMessageModuleIconType { shift, backspace, space, enter };
|
||||
#define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50
|
||||
#define CANNED_MESSAGE_MODULE_MESSAGES_SIZE 800
|
||||
|
||||
#ifndef CANNED_MESSAGE_MODULE_ENABLE
|
||||
#define CANNED_MESSAGE_MODULE_ENABLE 0
|
||||
#endif
|
||||
|
||||
// ============================
|
||||
// Data Structures
|
||||
// ============================
|
||||
|
||||
@@ -442,6 +442,7 @@ ExternalNotificationModule::ExternalNotificationModule()
|
||||
|
||||
ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
// Trigger external notification if enabled and not muted; isSilenced is from temporary mute toggles
|
||||
if (moduleConfig.external_notification.enabled && !isSilenced) {
|
||||
#ifdef T_WATCH_S3
|
||||
drv.setWaveform(0, 75);
|
||||
@@ -456,6 +457,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
|
||||
for (size_t i = 0; i < p.payload.size; i++) {
|
||||
if (p.payload.bytes[i] == ASCII_BELL) {
|
||||
containsBell = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,90 +467,47 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
|
||||
// If we receive a broadcast message, apply channel mute setting
|
||||
// If we receive a direct message and the receipent is us, apply DM mute setting
|
||||
// Else we just handle it as not muted.
|
||||
const bool directToUs = !isBroadcast(mp.to) && isToUs(&mp);
|
||||
bool is_muted = directToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0))
|
||||
: (ch.settings.has_module_settings && ch.settings.module_settings.is_muted);
|
||||
const bool isDmToUs = !isBroadcast(mp.to) && isToUs(&mp);
|
||||
bool is_muted = isDmToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0))
|
||||
: (ch.settings.has_module_settings && ch.settings.module_settings.is_muted);
|
||||
|
||||
if (moduleConfig.external_notification.alert_bell) {
|
||||
if (containsBell) {
|
||||
LOG_INFO("externalNotificationModule - Notification Bell");
|
||||
const bool buzzerModeIsDirectOnly =
|
||||
(config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY);
|
||||
|
||||
if (containsBell || !is_muted) {
|
||||
if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message ||
|
||||
moduleConfig.external_notification.alert_bell_vibra ||
|
||||
moduleConfig.external_notification.alert_message_vibra ||
|
||||
((moduleConfig.external_notification.alert_bell_buzzer ||
|
||||
moduleConfig.external_notification.alert_message_buzzer) &&
|
||||
canBuzz())) {
|
||||
nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout
|
||||
? (moduleConfig.external_notification.nag_timeout * 1000)
|
||||
: moduleConfig.external_notification.output_ms);
|
||||
LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff);
|
||||
isNagging = true;
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module or Bell");
|
||||
setExternalState(0, true);
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_bell_vibra) {
|
||||
if (containsBell) {
|
||||
LOG_INFO("externalNotificationModule - Notification Bell (Vibra)");
|
||||
isNagging = true;
|
||||
if (moduleConfig.external_notification.alert_bell_vibra ||
|
||||
moduleConfig.external_notification.alert_message_vibra) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module or Bell (Vibra)");
|
||||
setExternalState(1, true);
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
}
|
||||
|
||||
if ((moduleConfig.external_notification.alert_bell_buzzer ||
|
||||
moduleConfig.external_notification.alert_message_buzzer) &&
|
||||
canBuzz()) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module or Bell (Buzzer)");
|
||||
if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) {
|
||||
LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY");
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_bell_buzzer && canBuzz()) {
|
||||
if (containsBell) {
|
||||
LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)");
|
||||
isNagging = true;
|
||||
if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
setExternalState(2, true);
|
||||
} else {
|
||||
#ifdef HAS_I2S
|
||||
if (moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
|
||||
} else
|
||||
#endif
|
||||
if (moduleConfig.external_notification.use_pwm) {
|
||||
rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
|
||||
}
|
||||
}
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_message && !is_muted) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module");
|
||||
isNagging = true;
|
||||
setExternalState(0, true);
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_message_vibra && !is_muted) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module (Vibra)");
|
||||
isNagging = true;
|
||||
setExternalState(1, true);
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_message_buzzer && !is_muted) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module (Buzzer)");
|
||||
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
|
||||
(!isBroadcast(mp.to) && isToUs(&mp))) {
|
||||
// Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us
|
||||
isNagging = true;
|
||||
// Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us
|
||||
#ifdef T_LORA_PAGER
|
||||
if (canBuzz()) {
|
||||
drv.setWaveform(0, 16); // Long buzzer 100%
|
||||
drv.setWaveform(1, 0); // Pause
|
||||
drv.setWaveform(2, 16);
|
||||
@@ -558,11 +517,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
|
||||
drv.setWaveform(6, 16);
|
||||
drv.setWaveform(7, 0);
|
||||
drv.go();
|
||||
}
|
||||
#endif
|
||||
if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
setExternalState(2, true);
|
||||
} else {
|
||||
#ifdef HAS_I2S
|
||||
if (moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
|
||||
@@ -570,18 +525,13 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
|
||||
#endif
|
||||
if (moduleConfig.external_notification.use_pwm) {
|
||||
rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
|
||||
} else {
|
||||
setExternalState(2, true);
|
||||
}
|
||||
}
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
} else {
|
||||
// Don't beep if buzzer mode is "direct messages only" and it is no direct message
|
||||
LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY");
|
||||
}
|
||||
}
|
||||
|
||||
setIntervalFromNow(0); // run once so we know if we should do something
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
#include "configuration.h"
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
#include "buzz/BuzzerFeedbackThread.h"
|
||||
#include "input/ExpressLRSFiveWay.h"
|
||||
#include "input/InputBroker.h"
|
||||
#include "input/RotaryEncoderImpl.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/SerialKeyboardImpl.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#include "input/i2cButton.h"
|
||||
#include "modules/SystemCommandsModule.h"
|
||||
#if HAS_TRACKBALL
|
||||
#include "input/TrackballInterruptImpl1.h"
|
||||
#endif
|
||||
|
||||
#include "modules/StatusLEDModule.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C
|
||||
#include "input/cardKbI2cImpl.h"
|
||||
#endif
|
||||
#include "input/kbMatrixImpl.h"
|
||||
#include "modules/SystemCommandsModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_PKI
|
||||
#include "KeyVerificationModule.h"
|
||||
@@ -59,8 +43,6 @@
|
||||
#include "modules/WaypointModule.h"
|
||||
#endif
|
||||
#if ARCH_PORTDUINO
|
||||
#include "input/LinuxInputImpl.h"
|
||||
#include "input/SeesawRotary.h"
|
||||
#include "modules/Telemetry/HostMetrics.h"
|
||||
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||
#include "modules/StoreForwardModule.h"
|
||||
@@ -109,6 +91,9 @@
|
||||
#include "modules/DropzoneModule.h"
|
||||
#endif
|
||||
|
||||
#if defined(HAS_HARDWARE_WATCHDOG)
|
||||
#include "watchdog/watchdogThread.h"
|
||||
#endif
|
||||
/**
|
||||
* Create module instances here. If you are adding a new module, you must 'new' it here (or somewhere else)
|
||||
*/
|
||||
@@ -179,63 +164,6 @@ void setupModules()
|
||||
#endif
|
||||
// Example: Put your module here
|
||||
// new ReplyModule();
|
||||
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
#if defined(T_LORA_PAGER)
|
||||
// use a special FSM based rotary encoder version for T-LoRa Pager
|
||||
rotaryEncoderImpl = new RotaryEncoderImpl();
|
||||
if (!rotaryEncoderImpl->init()) {
|
||||
delete rotaryEncoderImpl;
|
||||
rotaryEncoderImpl = nullptr;
|
||||
}
|
||||
#elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2)
|
||||
upDownInterruptImpl1 = new UpDownInterruptImpl1();
|
||||
if (!upDownInterruptImpl1->init()) {
|
||||
delete upDownInterruptImpl1;
|
||||
upDownInterruptImpl1 = nullptr;
|
||||
}
|
||||
#else
|
||||
rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
|
||||
if (!rotaryEncoderInterruptImpl1->init()) {
|
||||
delete rotaryEncoderInterruptImpl1;
|
||||
rotaryEncoderInterruptImpl1 = nullptr;
|
||||
}
|
||||
#endif
|
||||
cardKbI2cImpl = new CardKbI2cImpl();
|
||||
cardKbI2cImpl->init();
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
i2cButton = new i2cButtonThread("i2cButtonThread");
|
||||
#endif
|
||||
#ifdef INPUTBROKER_MATRIX_TYPE
|
||||
kbMatrixImpl = new KbMatrixImpl();
|
||||
kbMatrixImpl->init();
|
||||
#endif // INPUTBROKER_MATRIX_TYPE
|
||||
#ifdef INPUTBROKER_SERIAL_TYPE
|
||||
aSerialKeyboardImpl = new SerialKeyboardImpl();
|
||||
aSerialKeyboardImpl->init();
|
||||
#endif // INPUTBROKER_MATRIX_TYPE
|
||||
}
|
||||
#endif // HAS_BUTTON
|
||||
#if ARCH_PORTDUINO
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") {
|
||||
seesawRotary = new SeesawRotary("SeesawRotary");
|
||||
if (!seesawRotary->init()) {
|
||||
delete seesawRotary;
|
||||
seesawRotary = nullptr;
|
||||
}
|
||||
aLinuxInputImpl = new LinuxInputImpl();
|
||||
aLinuxInputImpl->init();
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
trackballInterruptImpl1 = new TrackballInterruptImpl1();
|
||||
trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS);
|
||||
}
|
||||
#endif
|
||||
#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE
|
||||
expressLRSFiveWayInput = new ExpressLRSFiveWay();
|
||||
#endif
|
||||
#if HAS_SCREEN && !MESHTASTIC_EXCLUDE_CANNEDMESSAGES
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
cannedMessageModule = new CannedMessageModule();
|
||||
@@ -304,6 +232,9 @@ void setupModules()
|
||||
#if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS
|
||||
if (moduleConfig.has_range_test && moduleConfig.range_test.enabled)
|
||||
new RangeTestModule();
|
||||
#endif
|
||||
#if defined(HAS_HARDWARE_WATCHDOG)
|
||||
watchdogThread = new WatchdogThread();
|
||||
#endif
|
||||
// NOTE! This module must be added LAST because it likes to check for replies from other modules and avoid sending extra
|
||||
// acks
|
||||
|
||||
@@ -67,6 +67,8 @@ uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp)
|
||||
#if !(EVENTMODE) // This falls through to the default.
|
||||
return hopsUsed; // If the request used more hops than the limit, use the same amount of hops
|
||||
#endif
|
||||
} else if (mp.hop_start == 0) {
|
||||
return 0; // The requesting node wanted 0 hops, so the response also uses a direct/local path.
|
||||
} else if ((uint8_t)(hopsUsed + 2) < config.lora.hop_limit) {
|
||||
return hopsUsed + 2; // Use only the amount of hops needed with some margin as the way back may be different
|
||||
}
|
||||
|
||||
@@ -63,29 +63,26 @@
|
||||
SerialModule *serialModule;
|
||||
SerialModuleRadio *serialModuleRadio;
|
||||
|
||||
#if defined(TTGO_T_ECHO) || defined(TTGO_T_ECHO_PLUS) || defined(CANARYONE) || defined(MESHLINK) || \
|
||||
defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M5) || \
|
||||
defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || defined(ELECROW_ThinkNode_M3) || defined(MUZI_BASE)
|
||||
|
||||
SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial")
|
||||
{
|
||||
api_type = TYPE_SERIAL;
|
||||
}
|
||||
static Print *serialPrint = &Serial;
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32C6) || defined(RAK3172) || defined(EBYTE_E77_MBL)
|
||||
SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("Serial")
|
||||
{
|
||||
api_type = TYPE_SERIAL;
|
||||
}
|
||||
static Print *serialPrint = &Serial1;
|
||||
#else
|
||||
SerialModule::SerialModule() : StreamAPI(&Serial2), concurrency::OSThread("Serial")
|
||||
{
|
||||
api_type = TYPE_SERIAL;
|
||||
}
|
||||
static Print *serialPrint = &Serial2;
|
||||
#ifndef SERIAL_PRINT_PORT
|
||||
#define SERIAL_PRINT_PORT 2
|
||||
#endif
|
||||
|
||||
#if SERIAL_PRINT_PORT == 0
|
||||
#define SERIAL_PRINT_OBJECT Serial
|
||||
#elif SERIAL_PRINT_PORT == 1
|
||||
#define SERIAL_PRINT_OBJECT Serial1
|
||||
#elif SERIAL_PRINT_PORT == 2
|
||||
#define SERIAL_PRINT_OBJECT Serial2
|
||||
#else
|
||||
#error "Unsupported SERIAL_PRINT_PORT value. Allowed values are 0, 1, or 2."
|
||||
#endif
|
||||
|
||||
SerialModule::SerialModule() : StreamAPI(&SERIAL_PRINT_OBJECT), concurrency::OSThread("Serial")
|
||||
{
|
||||
api_type = TYPE_SERIAL;
|
||||
}
|
||||
static Print *serialPrint = &SERIAL_PRINT_OBJECT;
|
||||
|
||||
char serialBytes[512];
|
||||
size_t serialPayloadSize;
|
||||
|
||||
@@ -205,9 +202,7 @@ int32_t SerialModule::runOnce()
|
||||
Serial.begin(baud);
|
||||
Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
|
||||
}
|
||||
#elif !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \
|
||||
!defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \
|
||||
!defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE)
|
||||
#elif SERIAL_PRINT_PORT != 0
|
||||
|
||||
if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
|
||||
#ifdef ARCH_RP2040
|
||||
@@ -264,9 +259,7 @@ int32_t SerialModule::runOnce()
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \
|
||||
!defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \
|
||||
!defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE)
|
||||
#if SERIAL_PRINT_PORT != 0
|
||||
else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) {
|
||||
processWXSerial();
|
||||
|
||||
@@ -540,11 +533,7 @@ ParsedLine parseLine(const char *line)
|
||||
*/
|
||||
void SerialModule::processWXSerial()
|
||||
{
|
||||
#if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \
|
||||
!defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && \
|
||||
!defined(ELECROW_ThinkNode_M3) && \
|
||||
!defined(ELECROW_ThinkNode_M4) && \
|
||||
!defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) && !defined(MUZI_BASE)
|
||||
#if SERIAL_PRINT_PORT != 0 && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
|
||||
static unsigned int lastAveraged = 0;
|
||||
static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded.
|
||||
|
||||
@@ -130,7 +130,6 @@ int32_t StatusLEDModule::runOnce()
|
||||
#ifdef LED_CHARGE
|
||||
digitalWrite(LED_CHARGE, CHARGE_LED_state);
|
||||
#endif
|
||||
// digitalWrite(green_LED_PIN, LED_STATE_OFF);
|
||||
#ifdef LED_PAIRING
|
||||
digitalWrite(LED_PAIRING, PAIRING_LED_state);
|
||||
#endif
|
||||
|
||||
@@ -529,37 +529,46 @@ bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPac
|
||||
|
||||
bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m)
|
||||
{
|
||||
bool valid = true;
|
||||
bool valid = false;
|
||||
bool hasSensor = false;
|
||||
// getMetrics() doesn't always get evaluated because of
|
||||
// short-circuit evaluation rules in c++
|
||||
bool get_metrics;
|
||||
m->time = getTime();
|
||||
m->which_variant = meshtastic_Telemetry_environment_metrics_tag;
|
||||
m->variant.environment_metrics = meshtastic_EnvironmentMetrics_init_zero;
|
||||
|
||||
for (TelemetrySensor *sensor : sensors) {
|
||||
valid = valid && sensor->getMetrics(m);
|
||||
get_metrics = sensor->getMetrics(m); // avoid short-circuit evaluation rules
|
||||
valid = valid || get_metrics;
|
||||
hasSensor = true;
|
||||
}
|
||||
|
||||
#ifndef T1000X_SENSOR_EN
|
||||
if (ina219Sensor.hasSensor()) {
|
||||
valid = valid && ina219Sensor.getMetrics(m);
|
||||
get_metrics = ina219Sensor.getMetrics(m);
|
||||
valid = valid || get_metrics;
|
||||
hasSensor = true;
|
||||
}
|
||||
if (ina260Sensor.hasSensor()) {
|
||||
valid = valid && ina260Sensor.getMetrics(m);
|
||||
get_metrics = ina260Sensor.getMetrics(m);
|
||||
valid = valid || get_metrics;
|
||||
hasSensor = true;
|
||||
}
|
||||
if (ina3221Sensor.hasSensor()) {
|
||||
valid = valid && ina3221Sensor.getMetrics(m);
|
||||
get_metrics = ina3221Sensor.getMetrics(m);
|
||||
valid = valid || get_metrics;
|
||||
hasSensor = true;
|
||||
}
|
||||
if (max17048Sensor.hasSensor()) {
|
||||
valid = valid && max17048Sensor.getMetrics(m);
|
||||
get_metrics = max17048Sensor.getMetrics(m);
|
||||
valid = valid || get_metrics;
|
||||
hasSensor = true;
|
||||
}
|
||||
#endif
|
||||
#ifdef HAS_RAKPROT
|
||||
valid = valid && rak9154Sensor.getMetrics(m);
|
||||
get_metrics = rak9154Sensor.getMetrics(m);
|
||||
valid = valid || get_metrics;
|
||||
hasSensor = true;
|
||||
#endif
|
||||
return valid && hasSensor;
|
||||
|
||||
@@ -168,18 +168,21 @@ bool HealthTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
|
||||
bool HealthTelemetryModule::getHealthTelemetry(meshtastic_Telemetry *m)
|
||||
{
|
||||
bool valid = true;
|
||||
bool valid = false;
|
||||
bool hasSensor = false;
|
||||
bool get_metrics;
|
||||
m->time = getTime();
|
||||
m->which_variant = meshtastic_Telemetry_health_metrics_tag;
|
||||
m->variant.health_metrics = meshtastic_HealthMetrics_init_zero;
|
||||
|
||||
if (max30102Sensor.hasSensor()) {
|
||||
valid = valid && max30102Sensor.getMetrics(m);
|
||||
get_metrics = max30102Sensor.getMetrics(m);
|
||||
valid = valid || get_metrics; // avoid short-circuit evaluation rules
|
||||
hasSensor = true;
|
||||
}
|
||||
if (mlx90614Sensor.hasSensor()) {
|
||||
valid = valid && mlx90614Sensor.getMetrics(m);
|
||||
get_metrics = mlx90614Sensor.getMetrics(m);
|
||||
valid = valid || get_metrics;
|
||||
hasSensor = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ bool RAK12035Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
sensor.get_sensor_version(&data);
|
||||
if (data != 0) {
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
LOG_INFO("RAK12035Sensor Init Succeed \nSensor1 Firmware version: %i, Sensor Name: %s", data, sensorName);
|
||||
LOG_INFO("RAK12035Sensor Init Succeed \nSensor Firmware version: %i, Sensor Name: %s", data, sensorName);
|
||||
status = true;
|
||||
sensor.sensor_sleep();
|
||||
RESTORE_3V3_POWER();
|
||||
@@ -49,33 +49,39 @@ void RAK12035Sensor::setup()
|
||||
// TODO:: Check for and run calibration check for up to 2 additional sensors if present.
|
||||
uint16_t zero_val = 0;
|
||||
uint16_t hundred_val = 0;
|
||||
uint16_t default_zero_val = 550;
|
||||
uint16_t default_hundred_val = 420;
|
||||
const uint16_t default_zero_val = 510;
|
||||
const uint16_t default_hundred_val = 390;
|
||||
|
||||
sensor.sensor_on();
|
||||
sensor.begin();
|
||||
delay(200);
|
||||
sensor.get_dry_cal(&zero_val);
|
||||
delay(200);
|
||||
sensor.get_wet_cal(&hundred_val);
|
||||
delay(200);
|
||||
if (zero_val == 0 || zero_val <= hundred_val) {
|
||||
LOG_INFO("Dry calibration value is %d", zero_val);
|
||||
LOG_INFO("Wet calibration value is %d", hundred_val);
|
||||
LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: "
|
||||
"https://github.com/RAKWireless/RAK12035_SoilMoisture.");
|
||||
LOG_INFO("For now, setting default calibration value for Dry Calibration: %d", default_zero_val);
|
||||
|
||||
bool calibrationReset = false;
|
||||
|
||||
if (zero_val == 0) {
|
||||
LOG_INFO("Dry calibration not set, using default: %d", default_zero_val);
|
||||
sensor.set_dry_cal(default_zero_val);
|
||||
sensor.get_dry_cal(&zero_val);
|
||||
LOG_INFO("Dry calibration reset complete. New value is %d", zero_val);
|
||||
delay(200);
|
||||
zero_val = default_zero_val;
|
||||
calibrationReset = true;
|
||||
}
|
||||
if (hundred_val == 0 || hundred_val >= zero_val) {
|
||||
LOG_INFO("Dry calibration value is %d", zero_val);
|
||||
LOG_INFO("Wet calibration value is %d", hundred_val);
|
||||
LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: "
|
||||
"https://github.com/RAKWireless/RAK12035_SoilMoisture.");
|
||||
LOG_INFO("For now, setting default calibration value for Wet Calibration: %d", default_hundred_val);
|
||||
LOG_INFO("Wet calibration not set, using default: %d", default_hundred_val);
|
||||
sensor.set_wet_cal(default_hundred_val);
|
||||
sensor.get_wet_cal(&hundred_val);
|
||||
LOG_INFO("Wet calibration reset complete. New value is %d", hundred_val);
|
||||
delay(200);
|
||||
hundred_val = default_hundred_val;
|
||||
calibrationReset = true;
|
||||
}
|
||||
if (calibrationReset) {
|
||||
LOG_INFO("Default calibration values applied. Consider running the calibration sketch for better accuracy: "
|
||||
"https://github.com/RAKWireless/RAK12035_SoilMoisture");
|
||||
}
|
||||
|
||||
LOG_INFO("Dry calibration value: %d, Wet calibration value: %d", zero_val, hundred_val);
|
||||
sensor.sensor_sleep();
|
||||
RESTORE_3V3_POWER();
|
||||
delay(200);
|
||||
|
||||
@@ -686,6 +686,9 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
#ifdef NIMBLE_TWO
|
||||
if (ble->isDeInit)
|
||||
return;
|
||||
#else
|
||||
if (nimbleBluetooth && nimbleBluetooth->isDeInit)
|
||||
return;
|
||||
#endif
|
||||
|
||||
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
|
||||
|
||||
@@ -33,9 +33,6 @@
|
||||
#ifndef HAS_RADIO
|
||||
#define HAS_RADIO 1
|
||||
#endif
|
||||
#ifndef HAS_RTC
|
||||
#define HAS_RTC 1
|
||||
#endif
|
||||
#ifndef HAS_CPU_SHUTDOWN
|
||||
#define HAS_CPU_SHUTDOWN 1
|
||||
#endif
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user