Compare commits

..

3 Commits

Author SHA1 Message Date
Ben Meadors
1163855ce4 Merge branch 'develop' into big-mesh 2026-01-19 11:21:39 -06:00
Jonathan Bennett
65ba235360 Remove shorterTimeout check for NodeInfo sending
Shout out to @Xaositek for catching this one
2026-01-18 21:41:15 -06:00
Jonathan Bennett
4a5640b53d Slow down NodeInfo responses when on a "big" mesh 2026-01-18 16:10:34 -06:00
411 changed files with 2501 additions and 6261 deletions

1
.envrc
View File

@@ -1 +0,0 @@
use nix

View File

@@ -1,213 +0,0 @@
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
});

View File

@@ -1,139 +0,0 @@
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
View File

@@ -50,6 +50,3 @@ idf_component.yml
CMakeLists.txt CMakeLists.txt
/sdkconfig.* /sdkconfig.*
.dummy/* .dummy/*
# PYTHONPATH used by the Nix shell
.python3

View File

@@ -1,23 +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: 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 (122 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.

View File

@@ -0,0 +1,15 @@
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

View File

@@ -1,23 +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: 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 (122 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.

View File

@@ -0,0 +1,15 @@
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

View File

@@ -32,19 +32,6 @@ if ! command -v jq >/dev/null 2>&1; then
exit 1 exit 1
fi fi
# esptool v5 supports commands with dashes and deprecates commands with
# underscores. Prior versions only support commands with underscores
if ${ESPTOOL_CMD} | grep --quiet write-flash
then
ESPTOOL_WRITE_FLASH=write-flash
ESPTOOL_ERASE_FLASH=erase-flash
ESPTOOL_READ_FLASH_STATUS=read-flash-status
else
ESPTOOL_WRITE_FLASH=write_flash
ESPTOOL_ERASE_FLASH=erase_flash
ESPTOOL_READ_FLASH_STATUS=read_flash_status
fi
set -e set -e
# Usage info # Usage info
@@ -96,8 +83,8 @@ while [ $# -gt 0 ]; do
done done
if [[ $BPS_RESET == true ]]; then if [[ $BPS_RESET == true ]]; then
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset ${ESPTOOL_READ_FLASH_STATUS} $ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status
exit 0 exit 0
fi fi
[ -z "$FILENAME" ] && [ -n "$1" ] && { [ -z "$FILENAME" ] && [ -n "$1" ] && {
@@ -157,12 +144,12 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
fi fi
echo "Trying to flash ${FILENAME}, but first erasing and writing system information" echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
$ESPTOOL_CMD ${ESPTOOL_ERASE_FLASH} $ESPTOOL_CMD erase-flash
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $FIRMWARE_OFFSET "${FILENAME}" $ESPTOOL_CMD write-flash $FIRMWARE_OFFSET "${FILENAME}"
echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}" echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}"
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OTA_OFFSET "${OTAFILE}" $ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}"
echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}" echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}"
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OFFSET "${SPIFFSFILE}" $ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}"
else else
show_help show_help

View File

@@ -20,17 +20,6 @@ else
exit 1 exit 1
fi fi
# esptool v5 supports commands with dashes and deprecates commands with
# underscores. Prior versions only support commands with underscores
if ${ESPTOOL_CMD} | grep --quiet write-flash
then
ESPTOOL_WRITE_FLASH=write-flash
ESPTOOL_READ_FLASH_STATUS=read-flash-status
else
ESPTOOL_WRITE_FLASH=write_flash
ESPTOOL_READ_FLASH_STATUS=read_flash_status
fi
# Usage info # Usage info
show_help() { show_help() {
cat << EOF cat << EOF
@@ -80,7 +69,7 @@ done
shift "$((OPTIND-1))" shift "$((OPTIND-1))"
if [ "$CHANGE_MODE" = true ]; then if [ "$CHANGE_MODE" = true ]; then
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset ${ESPTOOL_READ_FLASH_STATUS} $ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status
exit 0 exit 0
fi fi
@@ -91,7 +80,7 @@ fi
if [[ -f "$FILENAME" && "$FILENAME" != *.factory.bin ]]; then if [[ -f "$FILENAME" && "$FILENAME" != *.factory.bin ]]; then
echo "Trying to flash update ${FILENAME}" echo "Trying to flash update ${FILENAME}"
$ESPTOOL_CMD --baud $FLASH_BAUD ${ESPTOOL_WRITE_FLASH} $UPDATE_OFFSET "${FILENAME}" $ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}"
else else
show_help show_help
echo "Invalid file: ${FILENAME}" echo "Invalid file: ${FILENAME}"

View File

@@ -87,9 +87,6 @@
</screenshots> </screenshots>
<releases> <releases>
<release version="2.7.19" date="2026-01-22">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.19</url>
</release>
<release version="2.7.18" date="2026-01-02"> <release version="2.7.18" date="2026-01-02">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.18</url> <url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.18</url>
</release> </release>

View File

@@ -1,50 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DMINIMESH_LITE -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x8029"],
["0x239A", "0x0029"],
["0x239A", "0x002A"]
],
"usb_product": "Minimesh Lite",
"mcu": "nrf52840",
"variant": "dls_Minimesh_Lite",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd"
},
"frameworks": ["arduino"],
"name": "Minimesh Lite",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["nrfutil", "jlink", "nrfjprog", "stlink"],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://deeplabstudio.com",
"vendor": "Deeplab Studio"
}

6
debian/changelog vendored
View File

@@ -1,9 +1,3 @@
meshtasticd (2.7.19.0) unstable; urgency=medium
* Version 2.7.19
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Thu, 22 Jan 2026 22:17:40 +0000
meshtasticd (2.7.18.0) unstable; urgency=medium meshtasticd (2.7.18.0) unstable; urgency=medium
* Version 2.7.18 * Version 2.7.18

44
flake.lock generated
View File

@@ -1,44 +0,0 @@
{
"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
}

View File

@@ -1,66 +0,0 @@
{
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
'';
};
}
);
};
}

View File

@@ -43,11 +43,13 @@ class Esp32C3ExceptionDecoder(DeviceMonitorFilterBase):
self.enabled = self.setup_paths() self.enabled = self.setup_paths()
if self.config.get("env:" + self.environment, "build_type") != "debug": if self.config.get("env:" + self.environment, "build_type") != "debug":
print(""" print(
"""
Please build project in debug configuration to get more details about an exception. Please build project in debug configuration to get more details about an exception.
See https://docs.platformio.org/page/projectconf/build_configurations.html See https://docs.platformio.org/page/projectconf/build_configurations.html
""") """
)
return self return self

View File

@@ -56,7 +56,6 @@ build_flags = -Wno-missing-field-initializers
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1 -DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
-DMESHTASTIC_EXCLUDE_POWERMON=1 -DMESHTASTIC_EXCLUDE_POWERMON=1
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
-DLED_BUILTIN=-1
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now #-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
#-D OLED_PL=1 #-D OLED_PL=1
#-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs #-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs
@@ -120,7 +119,7 @@ lib_deps =
[device-ui_base] [device-ui_base]
lib_deps = lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/63967a4a557d33d56fc5746f9128200dde2d88c5.zip https://github.com/meshtastic/device-ui/archive/3480b731d28b10d73414cf0dd7975bff745de8cf.zip
; Common libs for environmental measurements in telemetry module ; Common libs for environmental measurements in telemetry module
[environmental_base] [environmental_base]
@@ -213,30 +212,3 @@ lib_deps =
sensirion/Sensirion Core@0.7.2 sensirion/Sensirion Core@0.7.2
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0 sensirion/Sensirion I2C SCD4x@1.1.0
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
[environmental_extra_no_bsec]
lib_deps =
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
adafruit/Adafruit BMP3XX Library@2.1.6
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
adafruit/Adafruit MAX1704X@1.0.3
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
adafruit/Adafruit SHTC3 Library@1.0.2
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
adafruit/Adafruit LPS2X@2.0.6
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library
adafruit/Adafruit SHT31 Library@2.2.2
# renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library
adafruit/Adafruit VEML7700 Library@2.1.6
# renovate: datasource=custom.pio depName=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library
adafruit/Adafruit SHT4x Library@1.0.5
# renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
closedcube/ClosedCube OPT3001@1.1.2
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
sensirion/Sensirion Core@0.7.2
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0

View File

@@ -1,12 +0,0 @@
(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

View File

@@ -89,14 +89,22 @@ class BluetoothStatus : public Status
case ConnectionState::CONNECTED: case ConnectionState::CONNECTED:
LOG_DEBUG("BluetoothStatus CONNECTED"); LOG_DEBUG("BluetoothStatus CONNECTED");
#ifdef BLE_LED #ifdef BLE_LED
digitalWrite(BLE_LED, LED_STATE_ON); #ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, LOW);
#else
digitalWrite(BLE_LED, HIGH);
#endif
#endif #endif
break; break;
case ConnectionState::DISCONNECTED: case ConnectionState::DISCONNECTED:
LOG_DEBUG("BluetoothStatus DISCONNECTED"); LOG_DEBUG("BluetoothStatus DISCONNECTED");
#ifdef BLE_LED #ifdef BLE_LED
digitalWrite(BLE_LED, LED_STATE_OFF); #ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, HIGH);
#else
digitalWrite(BLE_LED, LOW);
#endif
#endif #endif
break; break;
} }

66
src/Led.cpp Normal file
View File

@@ -0,0 +1,66 @@
#include "Led.h"
#include "PowerMon.h"
#include "main.h"
#include "power.h"
GpioVirtPin ledForceOn, ledBlink;
#if defined(LED_PIN)
// Most boards have a GPIO for LED control
static GpioHwPin ledRawHwPin(LED_PIN);
#else
static GpioVirtPin ledRawHwPin; // Dummy pin for no hardware
#endif
#if LED_STATE_ON == 0
static GpioVirtPin ledHwPin;
static GpioNotTransformer ledInverter(&ledHwPin, &ledRawHwPin);
#else
static GpioPin &ledHwPin = ledRawHwPin;
#endif
#if defined(HAS_PMU)
/**
* A GPIO controlled by the PMU
*/
class GpioPmuPin : public GpioPin
{
public:
void set(bool value)
{
if (pmu_found && PMU) {
// blink the axp led
PMU->setChargingLedMode(value ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
}
}
} ledPmuHwPin;
// In some cases we need to drive a PMU LED and a normal LED
static GpioSplitter ledFinalPin(&ledHwPin, &ledPmuHwPin);
#else
static GpioPin &ledFinalPin = ledHwPin;
#endif
#ifdef USE_POWERMON
/**
* We monitor changes to the LED drive output because we use that as a sanity test in our power monitor stuff.
*/
class MonitoredLedPin : public GpioPin
{
public:
void set(bool value)
{
if (powerMon) {
if (value)
powerMon->setState(meshtastic_PowerMon_State_LED_On);
else
powerMon->clearState(meshtastic_PowerMon_State_LED_On);
}
ledFinalPin.set(value);
}
} monitoredLedPin;
#else
static GpioPin &monitoredLedPin = ledFinalPin;
#endif
static GpioBinaryTransformer ledForcer(&ledForceOn, &ledBlink, &monitoredLedPin, GpioBinaryTransformer::Or);

7
src/Led.h Normal file
View File

@@ -0,0 +1,7 @@
#include "GpioLogic.h"
#include "configuration.h"
/**
* ledForceOn and ledForceOff both override the normal ledBlinker behavior (which is controlled by main)
*/
extern GpioVirtPin ledForceOn, ledBlink;

View File

@@ -1,14 +1,11 @@
/** /**
* @file Power.cpp * @file Power.cpp
* @brief This file contains the implementation of the Power class, which is * @brief This file contains the implementation of the Power class, which is responsible for managing power-related functionality
* responsible for managing power-related functionality of the device. It * of the device. It includes battery level sensing, power management unit (PMU) control, and power state machine management. The
* includes battery level sensing, power management unit (PMU) control, and * Power class is used by the main device class to manage power-related functionality.
* power state machine management. The Power class is used by the main device
* class to manage power-related functionality.
* *
* The file also includes implementations of various battery level sensors, such * The file also includes implementations of various battery level sensors, such as the AnalogBatteryLevel class, which assumes
* as the AnalogBatteryLevel class, which assumes the battery voltage is * the battery voltage is attached via a voltage-divider to an analog input.
* attached via a voltage-divider to an analog input.
* *
* This file is part of the Meshtastic project. * This file is part of the Meshtastic project.
* For more information, see: https://meshtastic.org/ * For more information, see: https://meshtastic.org/
@@ -22,7 +19,6 @@
#include "configuration.h" #include "configuration.h"
#include "main.h" #include "main.h"
#include "meshUtils.h" #include "meshUtils.h"
#include "power/PowerHAL.h"
#include "sleep.h" #include "sleep.h"
#if defined(ARCH_PORTDUINO) #if defined(ARCH_PORTDUINO)
@@ -175,12 +171,22 @@ Power *power;
using namespace meshtastic; using namespace meshtastic;
// NRF52 has AREF_VOLTAGE defined in architecture.h but #ifndef AREF_VOLTAGE
// make sure it's included. If something is wrong with NRF52 #if defined(ARCH_NRF52)
// definition - compilation will fail on missing definition /*
#if !defined(AREF_VOLTAGE) && !defined(ARCH_NRF52) * Internal Reference is +/-0.6V, with an adjustable gain of 1/6, 1/5, 1/4,
* 1/3, 1/2 or 1, meaning 3.6, 3.0, 2.4, 1.8, 1.2 or 0.6V for the ADC levels.
*
* External Reference is VDD/4, with an adjustable gain of 1, 2 or 4, meaning
* VDD/4, VDD/2 or VDD for the ADC levels.
*
* Default settings are internal reference with 1/6 gain (GND..3.6V ADC range)
*/
#define AREF_VOLTAGE 3.6
#else
#define AREF_VOLTAGE 3.3 #define AREF_VOLTAGE 3.3
#endif #endif
#endif
/** /**
* If this board has a battery level sensor, set this to a valid implementation * If this board has a battery level sensor, set this to a valid implementation
@@ -227,8 +233,7 @@ static void battery_adcDisable()
#endif #endif
/** /**
* A simple battery level sensor that assumes the battery voltage is attached * A simple battery level sensor that assumes the battery voltage is attached via a voltage-divider to an analog input
* via a voltage-divider to an analog input
*/ */
class AnalogBatteryLevel : public HasBatteryLevel class AnalogBatteryLevel : public HasBatteryLevel
{ {
@@ -306,8 +311,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
#ifndef BATTERY_SENSE_SAMPLES #ifndef BATTERY_SENSE_SAMPLES
#define BATTERY_SENSE_SAMPLES \ #define BATTERY_SENSE_SAMPLES \
15 // Set the number of samples, it has an effect of increasing sensitivity in 15 // Set the number of samples, it has an effect of increasing sensitivity in complex electromagnetic environment.
// complex electromagnetic environment.
#endif #endif
#ifdef BATTERY_PIN #ifdef BATTERY_PIN
@@ -337,8 +341,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
battery_adcDisable(); battery_adcDisable();
if (!initial_read_done) { if (!initial_read_done) {
// Flush the smoothing filter with an ADC reading, if the reading is // Flush the smoothing filter with an ADC reading, if the reading is plausibly correct
// plausibly correct
if (scaled > last_read_value) if (scaled > last_read_value)
last_read_value = scaled; last_read_value = scaled;
initial_read_done = true; initial_read_done = true;
@@ -347,8 +350,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF
} }
// LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u", // LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u", BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t)
// BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t) (last_read_value)); // (last_read_value));
} }
return last_read_value; return last_read_value;
#endif // BATTERY_PIN #endif // BATTERY_PIN
@@ -417,8 +420,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
/** /**
* return true if there is a battery installed in this unit * return true if there is a battery installed in this unit
*/ */
// if we have a integrated device with a battery, we can assume that the // if we have a integrated device with a battery, we can assume that the battery is always connected
// battery is always connected
#ifdef BATTERY_IMMUTABLE #ifdef BATTERY_IMMUTABLE
virtual bool isBatteryConnect() override { return true; } virtual bool isBatteryConnect() override { return true; }
#elif defined(ADC_V) #elif defined(ADC_V)
@@ -439,10 +441,10 @@ class AnalogBatteryLevel : public HasBatteryLevel
virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; } virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
#endif #endif
/// If we see a battery voltage higher than physics allows - assume charger is /// If we see a battery voltage higher than physics allows - assume charger is pumping
/// pumping in power On some boards we don't have the power management chip /// in power
/// (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to detect external power /// On some boards we don't have the power management chip (like AXPxxxx)
/// source /// so we use EXT_PWR_DETECT GPIO pin to detect external power source
virtual bool isVbusIn() override virtual bool isVbusIn() override
{ {
#ifdef EXT_PWR_DETECT #ifdef EXT_PWR_DETECT
@@ -459,14 +461,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
} }
// if it's not HIGH - check the battery // if it's not HIGH - check the battery
#endif #endif
// If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it. #elif defined(MUZI_BASE)
return false; return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk;
// technically speaking this should work for all(?) NRF52 boards
// but needs testing across multiple devices. NRF52 USB would not even work if
// VBUS was not properly connected and detected by the CPU
#elif defined(MUZI_BASE) || defined(PROMICRO_DIY_TCXO)
return powerHAL_isVBUSConnected();
#endif #endif
return getBattVoltage() > chargingVolt; return getBattVoltage() > chargingVolt;
} }
@@ -489,9 +485,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
#else #else
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION) #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION)
if (hasINA()) { if (hasINA()) {
// get current flow from INA sensor - negative value means power flowing // get current flow from INA sensor - negative value means power flowing into the battery
// into the battery default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT // default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT RESISTOR <--> INA_VIN- <--> LOAD
// RESISTOR <--> INA_VIN- <--> LOAD
LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address); LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address);
#if defined(INA_CHARGING_DETECTION_INVERT) #if defined(INA_CHARGING_DETECTION_INVERT)
return getINACurrent() > 0; return getINACurrent() > 0;
@@ -507,8 +502,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
} }
private: private:
/// If we see a battery voltage higher than physics allows - assume charger is /// If we see a battery voltage higher than physics allows - assume charger is pumping
/// pumping in power /// in power
/// For heltecs with no battery connected, the measured voltage is 2204, so /// For heltecs with no battery connected, the measured voltage is 2204, so
// need to be higher than that, in this case is 2500mV (3000-500) // need to be higher than that, in this case is 2500mV (3000-500)
@@ -517,8 +512,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS; const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS;
// Start value from minimum voltage for the filter to not start from 0 // Start value from minimum voltage for the filter to not start from 0
// that could trigger some events. // that could trigger some events.
// This value is over-written by the first ADC reading, it the voltage seems // This value is over-written by the first ADC reading, it the voltage seems reasonable.
// reasonable.
bool initial_read_done = false; bool initial_read_done = false;
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS); float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
uint32_t last_read_time_ms = 0; uint32_t last_read_time_ms = 0;
@@ -660,8 +654,7 @@ bool Power::analogInit()
#ifdef CONFIG_IDF_TARGET_ESP32S3 #ifdef CONFIG_IDF_TARGET_ESP32S3
// ESP32S3 // ESP32S3
else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) { else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) {
LOG_INFO("ADC config based on Two Point values and fitting curve " LOG_INFO("ADC config based on Two Point values and fitting curve coefficients stored in eFuse");
"coefficients stored in eFuse");
} }
#endif #endif
else { else {
@@ -669,7 +662,13 @@ bool Power::analogInit()
} }
#endif // ARCH_ESP32 #endif // ARCH_ESP32
// NRF52 ADC init moved to powerHAL_init in nrf52 platform #ifdef ARCH_NRF52
#ifdef VBAT_AR_INTERNAL
analogReference(VBAT_AR_INTERNAL);
#else
analogReference(AR_INTERNAL); // 3.6V
#endif
#endif // ARCH_NRF52
#ifndef ARCH_ESP32 #ifndef ARCH_ESP32
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
@@ -724,16 +723,6 @@ bool Power::setup()
runASAP = true; runASAP = true;
}, },
CHANGE); CHANGE);
#endif
#ifdef EXT_CHRG_DETECT
attachInterrupt(
EXT_CHRG_DETECT,
[]() {
power->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
},
CHANGE);
#endif #endif
enabled = found; enabled = found;
low_voltage_counter = 0; low_voltage_counter = 0;
@@ -780,8 +769,7 @@ void Power::reboot()
HAL_NVIC_SystemReset(); HAL_NVIC_SystemReset();
#else #else
rebootAtMsec = -1; rebootAtMsec = -1;
LOG_WARN("FIXME implement reboot for this platform. Note that some settings " LOG_WARN("FIXME implement reboot for this platform. Note that some settings require a restart to be applied");
"require a restart to be applied");
#endif #endif
} }
@@ -791,12 +779,9 @@ void Power::shutdown()
#if HAS_SCREEN #if HAS_SCREEN
if (screen) { if (screen) {
#ifdef T_DECK_PRO #ifdef T_DECK_PRO
screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button
0); // T-Deck Pro has no power button
#elif defined(USE_EINK) #elif defined(USE_EINK)
screen->showSimpleBanner("Shutting Down...", screen->showSimpleBanner("Shutting Down...", 2250); // dismiss after 3 seconds to avoid the banner on the sleep screen
2250); // dismiss after 3 seconds to avoid the
// banner on the sleep screen
#else #else
screen->showSimpleBanner("Shutting Down...", 0); // stays on screen screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
#endif #endif
@@ -818,9 +803,6 @@ void Power::shutdown()
#endif #endif
#ifdef PIN_LED3 #ifdef PIN_LED3
ledOff(PIN_LED3); ledOff(PIN_LED3);
#endif
#ifdef LED_NOTIFICATION
ledOff(LED_NOTIFICATION);
#endif #endif
doDeepSleep(DELAY_FOREVER, true, true); doDeepSleep(DELAY_FOREVER, true, true);
#elif defined(ARCH_PORTDUINO) #elif defined(ARCH_PORTDUINO)
@@ -838,8 +820,7 @@ void Power::readPowerStatus()
int32_t batteryVoltageMv = -1; // Assume unknown int32_t batteryVoltageMv = -1; // Assume unknown
int8_t batteryChargePercent = -1; int8_t batteryChargePercent = -1;
OptionalBool usbPowered = OptUnknown; OptionalBool usbPowered = OptUnknown;
OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM code doesn't run every time
// code doesn't run every time
OptionalBool isChargingNow = OptUnknown; OptionalBool isChargingNow = OptUnknown;
if (batteryLevel) { if (batteryLevel) {
@@ -852,10 +833,9 @@ void Power::readPowerStatus()
if (batteryLevel->getBatteryPercent() >= 0) { if (batteryLevel->getBatteryPercent() >= 0) {
batteryChargePercent = batteryLevel->getBatteryPercent(); batteryChargePercent = batteryLevel->getBatteryPercent();
} else { } else {
// If the AXP192 returns a percentage less than 0, the feature is either // If the AXP192 returns a percentage less than 0, the feature is either not supported or there is an error
// not supported or there is an error In that case, we compute an // In that case, we compute an estimate of the charge percent based on open circuit voltage table defined
// estimate of the charge percent based on open circuit voltage table // in power.h
// defined in power.h
batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) / batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) /
((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))), ((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))),
0, 100); 0, 100);
@@ -863,12 +843,12 @@ void Power::readPowerStatus()
} }
} }
// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way // FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way better instead to make a Nrf52IsUsbPowered subclass
// better instead to make a Nrf52IsUsbPowered subclass (which shares a // (which shares a superclass with the BatteryLevel stuff)
// superclass with the BatteryLevel stuff) that just provides a few methods. But // that just provides a few methods. But in the interest of fixing this bug I'm going to follow current
// in the interest of fixing this bug I'm going to follow current practice. // practice.
#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates #ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates the power states. Takes 20 seconds or so to detect
// the power states. Takes 20 seconds or so to detect changes. // changes.
nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get(); nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get();
// LOG_DEBUG("NRF Power %d", nrf_usb_state); // LOG_DEBUG("NRF Power %d", nrf_usb_state);
@@ -942,9 +922,8 @@ void Power::readPowerStatus()
#endif #endif
// If we have a battery at all and it is less than 0%, force deep sleep if we // If we have a battery at all and it is less than 0%, force deep sleep if we have more than 10 low readings in
// have more than 10 low readings in a row. NOTE: min LiIon/LiPo voltage // a row. NOTE: min LiIon/LiPo voltage is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough.
// is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough.
// //
if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) { if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) {
@@ -966,8 +945,8 @@ int32_t Power::runOnce()
readPowerStatus(); readPowerStatus();
#ifdef HAS_PMU #ifdef HAS_PMU
// WE no longer use the IRQ line to wake the CPU (due to false wakes from // WE no longer use the IRQ line to wake the CPU (due to false wakes from sleep), but we do poll
// sleep), but we do poll the IRQ status by reading the registers over I2C // the IRQ status by reading the registers over I2C
if (PMU) { if (PMU) {
PMU->getIrqStatus(); PMU->getIrqStatus();
@@ -1009,8 +988,7 @@ int32_t Power::runOnce()
PMU->clearIrqStatus(); PMU->clearIrqStatus();
} }
#endif #endif
// Only read once every 20 seconds once the power status for the app has been // Only read once every 20 seconds once the power status for the app has been initialized
// initialized
return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME; return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME;
} }
@@ -1018,12 +996,10 @@ int32_t Power::runOnce()
* Init the power manager chip * Init the power manager chip
* *
* axp192 power * axp192 power
DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose comms to the axp192 because the OLED and the
comms to the axp192 because the OLED and the axp192 share the same i2c bus, axp192 share the same i2c bus, instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> ESP32 (keep this
instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> on!) LDO1 30mA -> charges GPS backup battery // charges the tiny J13 battery by the GPS to power the GPS ram (for a couple of
ESP32 (keep this on!) LDO1 30mA -> charges GPS backup battery // charges the days), can not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS
tiny J13 battery by the GPS to power the GPS ram (for a couple of days), can
not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS
* *
*/ */
bool Power::axpChipInit() bool Power::axpChipInit()
@@ -1068,10 +1044,9 @@ bool Power::axpChipInit()
if (!PMU) { if (!PMU) {
/* /*
* In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will * In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will be called at the same time.
* be called at the same time. In order not to affect other devices, if the * In order not to affect other devices, if the initialization of the PMU fails, Wire needs to be re-initialized once,
* initialization of the PMU fails, Wire needs to be re-initialized once, if * if there are multiple devices sharing the bus.
* there are multiple devices sharing the bus.
* * */ * * */
#ifndef PMU_USE_WIRE1 #ifndef PMU_USE_WIRE1
w->begin(I2C_SDA, I2C_SCL); w->begin(I2C_SDA, I2C_SCL);
@@ -1088,8 +1063,8 @@ bool Power::axpChipInit()
PMU->enablePowerOutput(XPOWERS_LDO2); PMU->enablePowerOutput(XPOWERS_LDO2);
// oled module power channel, // oled module power channel,
// disable it will cause abnormal communication between boot and AXP power // disable it will cause abnormal communication between boot and AXP power supply,
// supply, do not turn it off // do not turn it off
PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300); PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300);
// enable oled power // enable oled power
PMU->enablePowerOutput(XPOWERS_DCDC1); PMU->enablePowerOutput(XPOWERS_DCDC1);
@@ -1116,8 +1091,7 @@ bool Power::axpChipInit()
PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2); PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2);
} else if (PMU->getChipModel() == XPOWERS_AXP2101) { } else if (PMU->getChipModel() == XPOWERS_AXP2101) {
/*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it /*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it uses an AXP2101 power chip*/
* uses an AXP2101 power chip*/
if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) {
// Unuse power channel // Unuse power channel
PMU->disablePowerOutput(XPOWERS_DCDC2); PMU->disablePowerOutput(XPOWERS_DCDC2);
@@ -1152,8 +1126,8 @@ bool Power::axpChipInit()
// t-beam s3 core // t-beam s3 core
/** /**
* gnss module power channel * gnss module power channel
* The default ALDO4 is off, you need to turn on the GNSS power first, * The default ALDO4 is off, you need to turn on the GNSS power first, otherwise it will be invalid during
* otherwise it will be invalid during initialization * initialization
*/ */
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300); PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
PMU->enablePowerOutput(XPOWERS_ALDO4); PMU->enablePowerOutput(XPOWERS_ALDO4);
@@ -1203,8 +1177,7 @@ bool Power::axpChipInit()
// disable all axp chip interrupt // disable all axp chip interrupt
PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ); PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
// Set the constant current charging current of AXP2101, temporarily use // Set the constant current charging current of AXP2101, temporarily use 500mA by default
// 500mA by default
PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA); PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA);
// Set up the charging voltage // Set up the charging voltage
@@ -1270,12 +1243,11 @@ bool Power::axpChipInit()
PMU->getPowerChannelVoltage(XPOWERS_BLDO2)); PMU->getPowerChannelVoltage(XPOWERS_BLDO2));
} }
// We can safely ignore this approach for most (or all) boards because MCU // We can safely ignore this approach for most (or all) boards because MCU turned off
// turned off earlier than battery discharged to 2.6V. // earlier than battery discharged to 2.6V.
// //
// Unfortunately for now we can't use this killswitch for RAK4630-based boards // Unfortanly for now we can't use this killswitch for RAK4630-based boards because they have a bug with
// because they have a bug with battery voltage measurement. Probably it // battery voltage measurement. Probably it sometimes drops to low values.
// sometimes drops to low values.
#ifndef RAK4630 #ifndef RAK4630
// Set PMU shutdown voltage at 2.6V to maximize battery utilization // Set PMU shutdown voltage at 2.6V to maximize battery utilization
PMU->setSysPowerDownVoltage(2600); PMU->setSysPowerDownVoltage(2600);
@@ -1294,12 +1266,10 @@ bool Power::axpChipInit()
attachInterrupt( attachInterrupt(
PMU_IRQ, [] { pmu_irq = true; }, FALLING); PMU_IRQ, [] { pmu_irq = true; }, FALLING);
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ // we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ because it occurs repeatedly while there is
// because it occurs repeatedly while there is no battery also it could cause // no battery also it could cause inadvertent waking from light sleep just because the battery filled
// inadvertent waking from light sleep just because the battery filled we // we don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while no battery installed
// don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while // we don't look at AXPXXX_VBUS_REMOVED_IRQ because we don't have anything hooked to vbus
// no battery installed we don't look at AXPXXX_VBUS_REMOVED_IRQ because we
// don't have anything hooked to vbus
PMU->enableIRQ(pmuIrqMask); PMU->enableIRQ(pmuIrqMask);
PMU->clearIrqStatus(); PMU->clearIrqStatus();
@@ -1415,8 +1385,8 @@ class LipoCharger : public HasBatteryLevel
bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
if (result) { if (result) {
LOG_INFO("PPM BQ25896 init succeeded"); LOG_INFO("PPM BQ25896 init succeeded");
// Set the minimum operating voltage. Below this voltage, the PPM will // Set the minimum operating voltage. Below this voltage, the PPM will protect
// protect PPM->setSysPowerDownVoltage(3100); // PPM->setSysPowerDownVoltage(3100);
// Set input current limit, default is 500mA // Set input current limit, default is 500mA
// PPM->setInputCurrentLimit(800); // PPM->setInputCurrentLimit(800);
@@ -1439,8 +1409,7 @@ class LipoCharger : public HasBatteryLevel
PPM->enableMeasure(); PPM->enableMeasure();
// Turn on charging function // Turn on charging function
// If there is no battery connected, do not turn on the charging // If there is no battery connected, do not turn on the charging function
// function
PPM->enableCharge(); PPM->enableCharge();
} else { } else {
LOG_WARN("PPM BQ25896 init failed"); LOG_WARN("PPM BQ25896 init failed");
@@ -1475,8 +1444,7 @@ class LipoCharger : public HasBatteryLevel
virtual int getBatteryPercent() override virtual int getBatteryPercent() override
{ {
return -1; return -1;
// return bq->getChargePercent(); // don't use BQ27220 for battery percent, // return bq->getChargePercent(); // don't use BQ27220 for battery percent, it is not calibrated
// it is not calibrated
} }
/** /**
@@ -1598,8 +1566,7 @@ bool Power::meshSolarInit()
#else #else
/** /**
* The meshSolar battery level sensor is unavailable - default to * The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel
* AnalogBatteryLevel
*/ */
bool Power::meshSolarInit() bool Power::meshSolarInit()
{ {

View File

@@ -9,13 +9,13 @@
*/ */
#include "PowerFSM.h" #include "PowerFSM.h"
#include "Default.h" #include "Default.h"
#include "Led.h"
#include "MeshService.h" #include "MeshService.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "PowerMon.h" #include "PowerMon.h"
#include "configuration.h" #include "configuration.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "main.h" #include "main.h"
#include "modules/StatusLEDModule.h"
#include "sleep.h" #include "sleep.h"
#include "target_specific.h" #include "target_specific.h"
@@ -103,7 +103,7 @@ static void lsIdle()
uint32_t sleepTime = SLEEP_TIME; uint32_t sleepTime = SLEEP_TIME;
powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep); powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep);
statusLEDModule->setPowerLED(false); ledBlink.set(false); // Never leave led on while in light sleep
esp_sleep_source_t wakeCause2 = doLightSleep(sleepTime * 1000LL); esp_sleep_source_t wakeCause2 = doLightSleep(sleepTime * 1000LL);
powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep); powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep);
@@ -111,7 +111,7 @@ static void lsIdle()
case ESP_SLEEP_WAKEUP_TIMER: case ESP_SLEEP_WAKEUP_TIMER:
// Normal case: timer expired, we should just go back to sleep ASAP // Normal case: timer expired, we should just go back to sleep ASAP
statusLEDModule->setPowerLED(true); ledBlink.set(true); // briefly turn on led
wakeCause2 = doLightSleep(100); // leave led on for 1ms wakeCause2 = doLightSleep(100); // leave led on for 1ms
secsSlept += sleepTime; secsSlept += sleepTime;
@@ -146,7 +146,7 @@ static void lsIdle()
} }
} else { } else {
// Time to stop sleeping! // Time to stop sleeping!
statusLEDModule->setPowerLED(false); ledBlink.set(false);
LOG_INFO("Reached ls_secs, service loop()"); LOG_INFO("Reached ls_secs, service loop()");
powerFSM.trigger(EVENT_WAKE_TIMER); powerFSM.trigger(EVENT_WAKE_TIMER);
} }

View File

@@ -155,10 +155,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#endif #endif
// Default system gain to 0 if not defined // Default system gain to 0 if not defined
#ifndef NUM_PA_POINTS
#define NUM_PA_POINTS 1
#endif
#ifndef TX_GAIN_LORA #ifndef TX_GAIN_LORA
#define TX_GAIN_LORA 0 #define TX_GAIN_LORA 0
#endif #endif
@@ -390,6 +386,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#ifndef HAS_RADIO #ifndef HAS_RADIO
#define HAS_RADIO 0 #define HAS_RADIO 0
#endif #endif
#ifndef HAS_RTC
#define HAS_RTC 0
#endif
#ifndef HAS_CPU_SHUTDOWN #ifndef HAS_CPU_SHUTDOWN
#define HAS_CPU_SHUTDOWN 0 #define HAS_CPU_SHUTDOWN 0
#endif #endif
@@ -425,16 +424,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define HAS_RGB_LED #define HAS_RGB_LED
#endif #endif
#ifndef LED_STATE_OFF
#define LED_STATE_OFF 0
#endif
#ifndef LED_STATE_ON #ifndef LED_STATE_ON
#define LED_STATE_ON 1 #define LED_STATE_ON 1
#endif #endif
#ifndef LED_STATE_OFF
#define LED_STATE_OFF (LED_STATE_ON ^ 1)
#endif
#ifndef ledOff
#define ledOff(pin) pinMode(pin, INPUT)
#endif
// default mapping of pins // default mapping of pins
#if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN) #if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN)

View File

@@ -905,12 +905,6 @@ void GPS::writePinStandby(bool standby)
// Write and log // Write and log
pinMode(PIN_GPS_STANDBY, OUTPUT); pinMode(PIN_GPS_STANDBY, OUTPUT);
digitalWrite(PIN_GPS_STANDBY, val); digitalWrite(PIN_GPS_STANDBY, val);
// Enter backup mode on PA1010D; TODO: may be applicable to other MTK GPS too
if (IS_ONE_OF(gnssModel, GNSS_MODEL_MTK_PA1010D)) {
_serial_gps->write("$PMTK225,4*2F\r\n");
}
#ifdef GPS_DEBUG #ifdef GPS_DEBUG
LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW"); LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW");
#endif #endif

View File

@@ -276,7 +276,11 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
settimeofday(tv, NULL); settimeofday(tv, NULL);
#endif #endif
// nrf52 doesn't have a readable RTC (yet - software not written)
#if HAS_RTC
readFromRTC(); readFromRTC();
#endif
return RTCSetResultSuccess; return RTCSetResultSuccess;
} else { } else {
return RTCSetResultNotSet; // RTC was already set with a higher quality time return RTCSetResultNotSet; // RTC was already set with a higher quality time
@@ -393,7 +397,7 @@ uint32_t getValidTime(RTCQuality minQuality, bool local)
return (currentQuality >= minQuality) ? getTime(local) : 0; return (currentQuality >= minQuality) ? getTime(local) : 0;
} }
time_t gm_mktime(const struct tm *tm) time_t gm_mktime(struct tm *tm)
{ {
#if !MESHTASTIC_EXCLUDE_TZ #if !MESHTASTIC_EXCLUDE_TZ
time_t result = 0; time_t result = 0;
@@ -409,8 +413,8 @@ time_t gm_mktime(const struct tm *tm)
days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400); days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400);
// Now, within this tm->year, compute the days *before* this tm->month starts. // Now, within this tm->year, compute the days *before* this tm->month starts.
static const int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year
int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11 int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11
// If this is a leap year, and we're past February, add a day: // If this is a leap year, and we're past February, add a day:
if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) { if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) {
@@ -431,7 +435,6 @@ time_t gm_mktime(const struct tm *tm)
return result; return result;
#else #else
struct tm tmCopy = *tm; return mktime(tm);
return mktime(&tmCopy);
#endif #endif
} }

View File

@@ -54,7 +54,7 @@ uint32_t getValidTime(RTCQuality minQuality, bool local = false);
RTCSetResult readFromRTC(); RTCSetResult readFromRTC();
time_t gm_mktime(const struct tm *tm); time_t gm_mktime(struct tm *tm);
#define SEC_PER_DAY 86400 #define SEC_PER_DAY 86400
#define SEC_PER_HOUR 3600 #define SEC_PER_HOUR 3600

View File

@@ -1731,26 +1731,6 @@ int Screen::handleInputEvent(const InputEvent *event)
showFrame(FrameDirection::PREVIOUS); showFrame(FrameDirection::PREVIOUS);
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
showFrame(FrameDirection::NEXT); showFrame(FrameDirection::NEXT);
} else if (event->inputEvent == INPUT_BROKER_FN_F1) {
this->ui->switchToFrame(0);
lastScreenTransition = millis();
setFastFramerate();
} else if (event->inputEvent == INPUT_BROKER_FN_F2) {
this->ui->switchToFrame(1);
lastScreenTransition = millis();
setFastFramerate();
} else if (event->inputEvent == INPUT_BROKER_FN_F3) {
this->ui->switchToFrame(2);
lastScreenTransition = millis();
setFastFramerate();
} else if (event->inputEvent == INPUT_BROKER_FN_F4) {
this->ui->switchToFrame(3);
lastScreenTransition = millis();
setFastFramerate();
} else if (event->inputEvent == INPUT_BROKER_FN_F5) {
this->ui->switchToFrame(4);
lastScreenTransition = millis();
setFastFramerate();
} else if (event->inputEvent == INPUT_BROKER_UP_LONG) { } else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
// Long press up button for fast frame switching // Long press up button for fast frame switching
showPrevFrame(); showPrevFrame();

View File

@@ -266,8 +266,52 @@ void menuHandler::FrequencySlotPicker()
// Calculate number of channels (copied from RadioInterface::applyModemConfig()) // Calculate number of channels (copied from RadioInterface::applyModemConfig())
meshtastic_Config_LoRaConfig &loraConfig = config.lora; meshtastic_Config_LoRaConfig &loraConfig = config.lora;
double bw = loraConfig.use_preset ? modemPresetToBwKHz(loraConfig.modem_preset, myRegion->wideLora) double bw = loraConfig.bandwidth;
: bwCodeToKHz(loraConfig.bandwidth); if (loraConfig.use_preset) {
switch (loraConfig.modem_preset) {
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
bw = (myRegion->wideLora) ? 1625.0 : 500;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
bw = (myRegion->wideLora) ? 812.5 : 250;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
bw = (myRegion->wideLora) ? 812.5 : 250;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
bw = (myRegion->wideLora) ? 812.5 : 250;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
bw = (myRegion->wideLora) ? 812.5 : 250;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
bw = (myRegion->wideLora) ? 1625.0 : 500;
break;
default:
bw = (myRegion->wideLora) ? 812.5 : 250;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
bw = (myRegion->wideLora) ? 406.25 : 125;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
bw = (myRegion->wideLora) ? 406.25 : 125;
break;
}
} else {
bw = loraConfig.bandwidth;
if (bw == 31) // This parameter is not an integer
bw = 31.25;
if (bw == 62) // Fix for 62.5Khz bandwidth
bw = 62.5;
if (bw == 200)
bw = 203.125;
if (bw == 400)
bw = 406.25;
if (bw == 800)
bw = 812.5;
if (bw == 1600)
bw = 1625.0;
}
uint32_t numChannels = 0; uint32_t numChannels = 0;
if (myRegion) { if (myRegion) {

View File

@@ -140,7 +140,7 @@ struct ScreenColor {
uint8_t b; uint8_t b;
bool useVariant; bool useVariant;
explicit ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false) ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
: r(rIn), g(gIn), b(bIn), useVariant(variantIn) : r(rIn), g(gIn), b(bIn), useVariant(variantIn)
{ {
} }

View File

@@ -6,6 +6,7 @@
#include "MessageStore.h" #include "MessageStore.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "UIRenderer.h" #include "UIRenderer.h"
#include "configuration.h"
#include "gps/RTC.h" #include "gps/RTC.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/ScreenFonts.h" #include "graphics/ScreenFonts.h"
@@ -19,6 +20,7 @@
// External declarations // External declarations
extern bool hasUnreadMessage; extern bool hasUnreadMessage;
extern meshtastic_DeviceState devicestate;
extern graphics::Screen *screen; extern graphics::Screen *screen;
using graphics::Emote; using graphics::Emote;
@@ -47,7 +49,7 @@ static inline size_t utf8CharLen(uint8_t c)
} }
// Remove variation selectors (FE0F) and skin tone modifiers from emoji so they match your labels // Remove variation selectors (FE0F) and skin tone modifiers from emoji so they match your labels
static std::string normalizeEmoji(const std::string &s) std::string normalizeEmoji(const std::string &s)
{ {
std::string out; std::string out;
for (size_t i = 0; i < s.size();) { for (size_t i = 0; i < s.size();) {
@@ -80,7 +82,6 @@ uint32_t pauseStart = 0;
bool waitingToReset = false; bool waitingToReset = false;
bool scrollStarted = false; bool scrollStarted = false;
static bool didReset = false; static bool didReset = false;
static constexpr int MESSAGE_BLOCK_GAP = 6;
void scrollUp() void scrollUp()
{ {
@@ -110,6 +111,22 @@ void scrollDown()
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
{ {
std::string renderLine;
for (size_t i = 0; i < line.size();) {
uint8_t c = (uint8_t)line[i];
size_t len = utf8CharLen(c);
if (c == 0xEF && i + 2 < line.size() && (uint8_t)line[i + 1] == 0xB8 && (uint8_t)line[i + 2] == 0x8F) {
i += 3;
continue;
}
if (c == 0xF0 && i + 3 < line.size() && (uint8_t)line[i + 1] == 0x9F && (uint8_t)line[i + 2] == 0x8F &&
((uint8_t)line[i + 3] >= 0xBB && (uint8_t)line[i + 3] <= 0xBF)) {
i += 4;
continue;
}
renderLine.append(line, i, len);
i += len;
}
int cursorX = x; int cursorX = x;
const int fontHeight = FONT_HEIGHT_SMALL; const int fontHeight = FONT_HEIGHT_SMALL;
@@ -186,7 +203,8 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
// Render the emote (if found) // Render the emote (if found)
if (matchedEmote && i == nextEmotePos) { if (matchedEmote && i == nextEmotePos) {
int iconY = y + (lineHeight - matchedEmote->height) / 2; // Vertically center emote relative to font baseline (not just midline)
int iconY = fontY + (fontHeight - matchedEmote->height) / 2;
display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap);
cursorX += matchedEmote->width + 1; cursorX += matchedEmote->width + 1;
i += emojiLen; i += emojiLen;
@@ -405,63 +423,6 @@ static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &
return totalWidth; return totalWidth;
} }
struct MessageBlock {
size_t start;
size_t end;
bool mine;
};
static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool isHeaderLine)
{
if (isHeaderLine) {
return lineTopY + (FONT_HEIGHT_SMALL - 1);
}
int tallest = FONT_HEIGHT_SMALL;
for (int e = 0; e < numEmotes; ++e) {
if (line.find(emotes[e].label) != std::string::npos) {
if (emotes[e].height > tallest)
tallest = emotes[e].height;
}
}
const int lineHeight = std::max(FONT_HEIGHT_SMALL, tallest);
const int iconTop = lineTopY + (lineHeight - tallest) / 2;
return iconTop + tallest - 1;
}
static std::vector<MessageBlock> buildMessageBlocks(const std::vector<bool> &isHeaderVec, const std::vector<bool> &isMineVec)
{
std::vector<MessageBlock> blocks;
if (isHeaderVec.empty())
return blocks;
size_t start = 0;
bool mine = isMineVec[0];
for (size_t i = 1; i < isHeaderVec.size(); ++i) {
if (isHeaderVec[i]) {
MessageBlock b;
b.start = start;
b.end = i - 1;
b.mine = mine;
blocks.push_back(b);
start = i;
mine = isMineVec[i];
}
}
MessageBlock last;
last.start = start;
last.end = isHeaderVec.size() - 1;
last.mine = mine;
blocks.push_back(last);
return blocks;
}
static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int totalHeight, int scrollOffset, int startY) static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int totalHeight, int scrollOffset, int startY)
{ {
if (totalHeight <= visibleHeight) if (totalHeight <= visibleHeight)
@@ -521,14 +482,9 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
constexpr int LEFT_MARGIN = 2; constexpr int LEFT_MARGIN = 2;
constexpr int RIGHT_MARGIN = 2; constexpr int RIGHT_MARGIN = 2;
constexpr int SCROLLBAR_WIDTH = 3; constexpr int SCROLLBAR_WIDTH = 3;
constexpr int BUBBLE_PAD_X = 3;
constexpr int BUBBLE_PAD_Y = 4;
constexpr int BUBBLE_RADIUS = 4;
constexpr int BUBBLE_MIN_W = 24;
constexpr int BUBBLE_TEXT_INDENT = 2;
// Derived widths const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN;
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (BUBBLE_PAD_X * 2);
const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH; const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH;
// Title string depending on mode // Title string depending on mode
@@ -591,28 +547,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
char chanType[32] = ""; char chanType[32] = "";
if (currentMode == ThreadMode::ALL) { if (currentMode == ThreadMode::ALL) {
if (m.dest == NODENUM_BROADCAST) { if (m.dest == NODENUM_BROADCAST) {
const char *name = channels.getName(m.channelIndex); snprintf(chanType, sizeof(chanType), "#%s", channels.getName(m.channelIndex));
if (currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) {
if (strcmp(name, "ShortTurbo") == 0)
name = "ShortT";
else if (strcmp(name, "ShortSlow") == 0)
name = "ShortS";
else if (strcmp(name, "ShortFast") == 0)
name = "ShortF";
else if (strcmp(name, "MediumSlow") == 0)
name = "MedS";
else if (strcmp(name, "MediumFast") == 0)
name = "MedF";
else if (strcmp(name, "LongSlow") == 0)
name = "LongS";
else if (strcmp(name, "LongFast") == 0)
name = "LongF";
else if (strcmp(name, "LongTurbo") == 0)
name = "LongT";
else if (strcmp(name, "LongMod") == 0)
name = "LongM";
}
snprintf(chanType, sizeof(chanType), "#%s", name);
} else { } else {
snprintf(chanType, sizeof(chanType), "(DM)"); snprintf(chanType, sizeof(chanType), "(DM)");
} }
@@ -679,8 +614,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
} }
// Shrink Sender name if needed // Shrink Sender name if needed
int availWidth = (mine ? rightTextWidth : leftTextWidth) - display->getStringWidth(timeBuf) - int availWidth = SCREEN_WIDTH - display->getStringWidth(timeBuf) - display->getStringWidth(chanType) -
display->getStringWidth(chanType) - display->getStringWidth(" @..."); display->getStringWidth(" @...") - 10;
if (availWidth < 0) if (availWidth < 0)
availWidth = 0; availWidth = 0;
@@ -732,8 +667,6 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
cachedLines = allLines; cachedLines = allLines;
cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader); cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader);
std::vector<MessageBlock> blocks = buildMessageBlocks(isHeader, isMine);
// Scrolling logic (unchanged) // Scrolling logic (unchanged)
int totalHeight = 0; int totalHeight = 0;
for (size_t i = 0; i < cachedHeights.size(); ++i) for (size_t i = 0; i < cachedHeights.size(); ++i)
@@ -781,133 +714,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int finalScroll = (int)scrollY; int finalScroll = (int)scrollY;
int yOffset = -finalScroll + getTextPositions(display)[1]; int yOffset = -finalScroll + getTextPositions(display)[1];
const int contentTop = getTextPositions(display)[1];
const int contentBottom = scrollBottom; // already excludes nav line
const int rightEdge = SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN;
const int bubbleGapY = std::max(1, MESSAGE_BLOCK_GAP / 2);
std::vector<int> lineTop;
lineTop.resize(cachedLines.size());
{
int acc = 0;
for (size_t i = 0; i < cachedLines.size(); ++i) {
lineTop[i] = yOffset + acc;
acc += cachedHeights[i];
}
}
// Draw bubbles
for (size_t bi = 0; bi < blocks.size(); ++bi) {
const auto &b = blocks[bi];
if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end)
continue;
int visualTop = lineTop[b.start];
int topY;
if (isHeader[b.start]) {
// Header start
constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2
topY = visualTop - BUBBLE_PAD_TOP_HEADER;
} else {
// Body start
bool thisLineHasEmote = false;
for (int e = 0; e < numEmotes; ++e) {
if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
thisLineHasEmote = true;
break;
}
}
if (thisLineHasEmote) {
constexpr int EMOTE_PADDING_ABOVE = 4;
visualTop -= EMOTE_PADDING_ABOVE;
}
topY = visualTop - BUBBLE_PAD_Y;
}
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
int bottomY = visualBottom + BUBBLE_PAD_Y;
if (bi + 1 < blocks.size()) {
int nextHeaderIndex = (int)blocks[bi + 1].start;
int nextTop = lineTop[nextHeaderIndex];
int maxBottom = nextTop - 1 - bubbleGapY;
if (bottomY > maxBottom)
bottomY = maxBottom;
}
if (bottomY <= topY + 2)
continue;
if (bottomY < contentTop || topY > contentBottom - 1)
continue;
int maxLineW = 0;
for (size_t i = b.start; i <= b.end; ++i) {
int w = 0;
if (isHeader[i]) {
w = display->getStringWidth(cachedLines[i].c_str());
if (b.mine)
w += 12; // room for ACK/NACK/relay mark
} else {
w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
}
if (w > maxLineW)
maxLineW = w;
}
int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (BUBBLE_PAD_X * 2));
int bubbleH = (bottomY - topY) + 1;
int bubbleX = 0;
if (b.mine) {
bubbleX = rightEdge - bubbleW;
} else {
bubbleX = x;
}
if (bubbleX < x)
bubbleX = x;
if (bubbleX + bubbleW > rightEdge)
bubbleW = std::max(1, rightEdge - bubbleX);
if (bubbleW > 1 && bubbleH > 1) {
int x1 = bubbleX + bubbleW - 1;
int y1 = topY + bubbleH - 1;
if (b.mine) {
// Send Message (Right side)
display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH);
// Top Right Corner
display->drawRect(x1 - 1, 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 + 2, topY - 1, 1, 1);
display->drawRect(x1 - bubbleW + 2, bottomY - 1, 1, 1);
display->setColor(WHITE);
} else {
// Received Message (Left Side)
display->drawRect(bubbleX, topY, bubbleW + 1, bubbleH);
// Top Left Corner
display->drawRect(bubbleX + 1, topY + 1, 2, 1);
display->drawRect(bubbleX + 1, topY + 1, 1, 2);
// Bottom Left Corner
display->drawRect(bubbleX + 1, bottomY - 1, 2, 1);
display->drawRect(bubbleX + 1, bottomY - 2, 1, 2);
// Knock the corners off to make a bubble
display->setColor(BLACK);
display->drawRect(bubbleX + bubbleW, topY, 1, 1);
display->drawRect(bubbleX + bubbleW, bottomY, 1, 1);
display->setColor(WHITE);
}
}
}
// Render visible lines // Render visible lines
int lineY = yOffset;
for (size_t i = 0; i < cachedLines.size(); ++i) { for (size_t i = 0; i < cachedLines.size(); ++i) {
int lineY = yOffset;
for (size_t j = 0; j < i; ++j)
lineY += cachedHeights[j];
if (lineY > -cachedHeights[i] && lineY < scrollBottom) { if (lineY > -cachedHeights[i] && lineY < scrollBottom) {
if (isHeader[i]) { if (isHeader[i]) {
@@ -916,28 +728,14 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int headerX; int headerX;
if (isMine[i]) { if (isMine[i]) {
// push header left to avoid overlap with scrollbar // push header left to avoid overlap with scrollbar
headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - BUBBLE_TEXT_INDENT; headerX = SCREEN_WIDTH - w - SCROLLBAR_WIDTH - RIGHT_MARGIN;
if (headerX < LEFT_MARGIN) if (headerX < LEFT_MARGIN)
headerX = LEFT_MARGIN; headerX = LEFT_MARGIN;
} else { } else {
headerX = x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT; headerX = x;
} }
display->drawString(headerX, lineY, cachedLines[i].c_str()); display->drawString(headerX, lineY, cachedLines[i].c_str());
// Draw underline just under header text
int underlineY = lineY + FONT_HEIGHT_SMALL;
int underlineW = w;
int maxW = rightEdge - headerX;
if (maxW < 0)
maxW = 0;
if (underlineW > maxW)
underlineW = maxW;
for (int px = 0; px < underlineW; ++px) {
display->setPixel(headerX + px, underlineY);
}
// Draw ACK/NACK mark for our own messages // Draw ACK/NACK mark for our own messages
if (isMine[i]) { if (isMine[i]) {
int markX = headerX - 10; int markX = headerX - 10;
@@ -955,28 +753,32 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// AckStatus::NONE → show nothing // AckStatus::NONE → show nothing
} }
// Draw underline just under header text
int underlineY = lineY + FONT_HEIGHT_SMALL;
for (int px = 0; px < w; ++px) {
display->setPixel(headerX + px, underlineY);
}
} else { } else {
// Render message line // Render message line
if (isMine[i]) { if (isMine[i]) {
// Calculate actual rendered width including emotes // Calculate actual rendered width including emotes
int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - BUBBLE_TEXT_INDENT; int rightX = SCREEN_WIDTH - renderedWidth - SCROLLBAR_WIDTH - RIGHT_MARGIN;
if (rightX < LEFT_MARGIN) if (rightX < LEFT_MARGIN)
rightX = LEFT_MARGIN; rightX = LEFT_MARGIN;
drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes); drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes);
} else { } else {
drawStringWithEmotes(display, x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT, lineY, cachedLines[i], emotes, drawStringWithEmotes(display, x, lineY, cachedLines[i], emotes, numEmotes);
numEmotes);
} }
} }
} }
lineY += cachedHeights[i];
} }
int totalContentHeight = totalHeight;
int visibleHeight = usableHeight;
// Draw scrollbar // Draw scrollbar
drawMessageScrollbar(display, usableHeight, totalHeight, finalScroll, getTextPositions(display)[1]); drawMessageScrollbar(display, visibleHeight, totalContentHeight, finalScroll, getTextPositions(display)[1]);
graphics::drawCommonHeader(display, x, y, titleStr); graphics::drawCommonHeader(display, x, y, titleStr);
graphics::drawCommonFooter(display, x, y); graphics::drawCommonFooter(display, x, y);
} }
@@ -1039,6 +841,7 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
constexpr int HEADER_UNDERLINE_GAP = 0; // space between underline and first body line constexpr int HEADER_UNDERLINE_GAP = 0; // space between underline and first body line
constexpr int HEADER_UNDERLINE_PIX = 1; // underline thickness (1px row drawn) constexpr int HEADER_UNDERLINE_PIX = 1; // underline thickness (1px row drawn)
constexpr int BODY_LINE_LEADING = -4; // default vertical leading for normal body lines constexpr int BODY_LINE_LEADING = -4; // default vertical leading for normal body lines
constexpr int MESSAGE_BLOCK_GAP = 4; // gap after a message block before a new header
constexpr int EMOTE_PADDING_ABOVE = 4; // space above emote line (added to line above) constexpr int EMOTE_PADDING_ABOVE = 4; // space above emote line (added to line above)
constexpr int EMOTE_PADDING_BELOW = 3; // space below emote line (added to emote line) constexpr int EMOTE_PADDING_BELOW = 3; // space below emote line (added to emote line)
@@ -1048,7 +851,6 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
for (size_t idx = 0; idx < lines.size(); ++idx) { for (size_t idx = 0; idx < lines.size(); ++idx) {
const auto &line = lines[idx]; const auto &line = lines[idx];
const int baseHeight = FONT_HEIGHT_SMALL; const int baseHeight = FONT_HEIGHT_SMALL;
int lineHeight = baseHeight;
// Detect if THIS line or NEXT line contains an emote // Detect if THIS line or NEXT line contains an emote
bool hasEmote = false; bool hasEmote = false;
@@ -1070,6 +872,8 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
} }
} }
int lineHeight = baseHeight;
if (isHeaderVec[idx]) { if (isHeaderVec[idx]) {
// Header line spacing // Header line spacing
lineHeight = baseHeight + HEADER_UNDERLINE_PIX + HEADER_UNDERLINE_GAP; lineHeight = baseHeight + HEADER_UNDERLINE_PIX + HEADER_UNDERLINE_GAP;
@@ -1118,7 +922,7 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
// Banner logic // Banner logic
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from);
char longName[48] = "?"; char longName[48] = "???";
if (node && node->user.long_name) { if (node && node->user.long_name) {
strncpy(longName, node->user.long_name, sizeof(longName) - 1); strncpy(longName, node->user.long_name, sizeof(longName) - 1);
longName[sizeof(longName) - 1] = '\0'; longName[sizeof(longName) - 1] = '\0';

View File

@@ -55,7 +55,7 @@ InkHUD::Tile *InkHUD::Applet::getTile()
} }
// Draw the applet // Draw the applet
void InkHUD::Applet::render(bool full) void InkHUD::Applet::render()
{ {
assert(assignedTile); // Ensure that we have a tile assert(assignedTile); // Ensure that we have a tile
assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile
@@ -65,11 +65,10 @@ void InkHUD::Applet::render(bool full)
wantRender = false; // Flag set by requestUpdate wantRender = false; // Flag set by requestUpdate
wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored. wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored.
wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted. wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted.
wantFullRender = true; // Default to a full render
updateDimensions(); updateDimensions();
resetDrawingSpace(); resetDrawingSpace();
onRender(full); // Draw the applet onRender(); // Derived applet's drawing takes place here
// Handle "Tile Highlighting" // Handle "Tile Highlighting"
// Some devices may use an auxiliary button to switch between tiles // Some devices may use an auxiliary button to switch between tiles
@@ -116,11 +115,6 @@ Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType()
return wantUpdateType; return wantUpdateType;
} }
bool InkHUD::Applet::wantsFullRender()
{
return wantFullRender;
}
// Get size of the applet's drawing space from its tile // Get size of the applet's drawing space from its tile
// Performed immediately before derived applet's drawing code runs // Performed immediately before derived applet's drawing code runs
void InkHUD::Applet::updateDimensions() void InkHUD::Applet::updateDimensions()
@@ -148,11 +142,10 @@ void InkHUD::Applet::resetDrawingSpace()
// Once the renderer has given other applets a chance to process whatever event we just detected, // Once the renderer has given other applets a chance to process whatever event we just detected,
// it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground) // it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground)
// We should requestUpdate even if our applet is currently background, because this might be changed by autoshow // We should requestUpdate even if our applet is currently background, because this might be changed by autoshow
void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type, bool full) void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type)
{ {
wantRender = true; wantRender = true;
wantUpdateType = type; wantUpdateType = type;
wantFullRender = full;
inkhud->requestUpdate(); inkhud->requestUpdate();
} }

View File

@@ -64,11 +64,10 @@ class Applet : public GFX
// Rendering // Rendering
void render(bool full); // Draw the applet void render(); // Draw the applet
bool wantsToRender(); // Check whether applet wants to render bool wantsToRender(); // Check whether applet wants to render
bool wantsToAutoshow(); // Check whether applet wants to become foreground bool wantsToAutoshow(); // Check whether applet wants to become foreground
Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer
bool wantsFullRender(); // Check whether applet wants to render over its previous render
void updateDimensions(); // Get current size from tile void updateDimensions(); // Get current size from tile
void resetDrawingSpace(); // Makes sure every render starts with same parameters void resetDrawingSpace(); // Makes sure every render starts with same parameters
@@ -83,7 +82,7 @@ class Applet : public GFX
// Event handlers // Event handlers
virtual void onRender(bool full) = 0; // For drawing the applet virtual void onRender() = 0; // All drawing happens here
virtual void onActivate() {} virtual void onActivate() {}
virtual void onDeactivate() {} virtual void onDeactivate() {}
virtual void onForeground() {} virtual void onForeground() {}
@@ -97,9 +96,6 @@ class Applet : public GFX
virtual void onNavDown() {} virtual void onNavDown() {}
virtual void onNavLeft() {} virtual void onNavLeft() {}
virtual void onNavRight() {} virtual void onNavRight() {}
virtual void onFreeText(char c) {}
virtual void onFreeTextDone() {}
virtual void onFreeTextCancel() {}
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
@@ -112,9 +108,8 @@ class Applet : public GFX
protected: protected:
void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED, void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update
bool full = true); // Ask WindowManager to schedule a display update void requestAutoshow(); // Ask for applet to be moved to foreground
void requestAutoshow(); // Ask for applet to be moved to foreground
uint16_t X(float f); // Map applet width, mapped from 0 to 1.0 uint16_t X(float f); // Map applet width, mapped from 0 to 1.0
uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0 uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0
@@ -169,7 +164,6 @@ class Applet : public GFX
bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground? bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground?
NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType = NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType =
NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display
bool wantFullRender = true; // Render with a fresh canvas
using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly
using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager. using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager.

View File

@@ -4,7 +4,7 @@
using namespace NicheGraphics; using namespace NicheGraphics;
void InkHUD::MapApplet::onRender(bool full) void InkHUD::MapApplet::onRender()
{ {
// Abort if no markers to render // Abort if no markers to render
if (!enoughMarkers()) { if (!enoughMarkers()) {

View File

@@ -27,7 +27,7 @@ namespace NicheGraphics::InkHUD
class MapApplet : public Applet class MapApplet : public Applet
{ {
public: public:
void onRender(bool full) override; void onRender() override;
protected: protected:
virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes

View File

@@ -103,7 +103,7 @@ uint8_t InkHUD::NodeListApplet::maxCards()
} }
// Draw, using info which derived applet placed into NodeListApplet::cards for us // Draw, using info which derived applet placed into NodeListApplet::cards for us
void InkHUD::NodeListApplet::onRender(bool full) void InkHUD::NodeListApplet::onRender()
{ {
// ================================ // ================================

View File

@@ -46,7 +46,7 @@ class NodeListApplet : public Applet, public MeshModule
public: public:
NodeListApplet(const char *name); NodeListApplet(const char *name);
void onRender(bool full) override; void onRender() override;
bool wantPacket(const meshtastic_MeshPacket *p) override; bool wantPacket(const meshtastic_MeshPacket *p) override;
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;

View File

@@ -6,7 +6,7 @@ using namespace NicheGraphics;
// All drawing happens here // All drawing happens here
// Our basic example doesn't do anything useful. It just passively prints some text. // Our basic example doesn't do anything useful. It just passively prints some text.
void InkHUD::BasicExampleApplet::onRender(bool full) void InkHUD::BasicExampleApplet::onRender()
{ {
printAt(0, 0, "Hello, World!"); printAt(0, 0, "Hello, World!");

View File

@@ -28,7 +28,7 @@ class BasicExampleApplet : public Applet
// You must have an onRender() method // You must have an onRender() method
// All drawing happens here // All drawing happens here
void onRender(bool full) override; void onRender() override;
}; };
} // namespace NicheGraphics::InkHUD } // namespace NicheGraphics::InkHUD

View File

@@ -35,7 +35,7 @@ ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_Mesh
// We can trigger a render by calling requestUpdate() // We can trigger a render by calling requestUpdate()
// Render might be called by some external source // Render might be called by some external source
// We should always be ready to draw // We should always be ready to draw
void InkHUD::NewMsgExampleApplet::onRender(bool full) void InkHUD::NewMsgExampleApplet::onRender()
{ {
printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0) printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0)

View File

@@ -34,7 +34,7 @@ class NewMsgExampleApplet : public Applet, public SinglePortModule
NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {} NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {}
// All drawing happens here // All drawing happens here
void onRender(bool full) override; void onRender() override;
// Your applet might also want to use some of these // Your applet might also want to use some of these
// Useful for setting up or tidying up // Useful for setting up or tidying up

View File

@@ -10,7 +10,7 @@ InkHUD::AlignStickApplet::AlignStickApplet()
bringToForeground(); bringToForeground();
} }
void InkHUD::AlignStickApplet::onRender(bool full) void InkHUD::AlignStickApplet::onRender()
{ {
setFont(fontMedium); setFont(fontMedium);
printAt(0, 0, "Align Joystick:"); printAt(0, 0, "Align Joystick:");
@@ -152,17 +152,19 @@ void InkHUD::AlignStickApplet::onBackground()
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
void InkHUD::AlignStickApplet::onButtonLongPress() void InkHUD::AlignStickApplet::onButtonLongPress()
{ {
sendToBackground(); sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
void InkHUD::AlignStickApplet::onExitLong() void InkHUD::AlignStickApplet::onExitLong()
{ {
sendToBackground(); sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
void InkHUD::AlignStickApplet::onNavUp() void InkHUD::AlignStickApplet::onNavUp()
@@ -170,6 +172,7 @@ void InkHUD::AlignStickApplet::onNavUp()
settings->joystick.aligned = true; settings->joystick.aligned = true;
sendToBackground(); sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
void InkHUD::AlignStickApplet::onNavDown() void InkHUD::AlignStickApplet::onNavDown()
@@ -178,6 +181,7 @@ void InkHUD::AlignStickApplet::onNavDown()
settings->joystick.aligned = true; settings->joystick.aligned = true;
sendToBackground(); sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
void InkHUD::AlignStickApplet::onNavLeft() void InkHUD::AlignStickApplet::onNavLeft()
@@ -186,6 +190,7 @@ void InkHUD::AlignStickApplet::onNavLeft()
settings->joystick.aligned = true; settings->joystick.aligned = true;
sendToBackground(); sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
void InkHUD::AlignStickApplet::onNavRight() void InkHUD::AlignStickApplet::onNavRight()
@@ -194,6 +199,7 @@ void InkHUD::AlignStickApplet::onNavRight()
settings->joystick.aligned = true; settings->joystick.aligned = true;
sendToBackground(); sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
#endif #endif

View File

@@ -23,7 +23,7 @@ class AlignStickApplet : public SystemApplet
public: public:
AlignStickApplet(); AlignStickApplet();
void onRender(bool full) override; void onRender() override;
void onForeground() override; void onForeground() override;
void onBackground() override; void onBackground() override;
void onButtonLongPress() override; void onButtonLongPress() override;

View File

@@ -6,8 +6,6 @@ using namespace NicheGraphics;
InkHUD::BatteryIconApplet::BatteryIconApplet() InkHUD::BatteryIconApplet::BatteryIconApplet()
{ {
alwaysRender = true; // render everytime the screen is updated
// Show at boot, if user has previously enabled the feature // Show at boot, if user has previously enabled the feature
if (settings->optionalFeatures.batteryIcon) if (settings->optionalFeatures.batteryIcon)
bringToForeground(); bringToForeground();
@@ -46,7 +44,7 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
return 0; // Tell Observable to continue informing other observers return 0; // Tell Observable to continue informing other observers
} }
void InkHUD::BatteryIconApplet::onRender(bool full) void InkHUD::BatteryIconApplet::onRender()
{ {
// Fill entire tile // Fill entire tile
// - size of icon controlled by size of tile // - size of icon controlled by size of tile

View File

@@ -23,7 +23,7 @@ class BatteryIconApplet : public SystemApplet
public: public:
BatteryIconApplet(); BatteryIconApplet();
void onRender(bool full) override; void onRender() override;
int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available
private: private:

View File

@@ -1,257 +0,0 @@
#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

View File

@@ -1,66 +0,0 @@
#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

View File

@@ -30,7 +30,7 @@ InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet")
// This is then drawn with a FULL refresh by Renderer::begin // This is then drawn with a FULL refresh by Renderer::begin
} }
void InkHUD::LogoApplet::onRender(bool full) void InkHUD::LogoApplet::onRender()
{ {
// Size of the region which the logo should "scale to fit" // Size of the region which the logo should "scale to fit"
uint16_t logoWLimit = X(0.8); uint16_t logoWLimit = X(0.8);
@@ -120,7 +120,7 @@ void InkHUD::LogoApplet::onBackground()
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
// Begin displaying the screen which is shown at shutdown // Begin displaying the screen which is shown at shutdown
@@ -138,10 +138,10 @@ void InkHUD::LogoApplet::onShutdown()
// Intention is to restore display health. // Intention is to restore display health.
inverted = true; inverted = true;
inkhud->forceUpdate(Drivers::EInk::FULL, true, false); inkhud->forceUpdate(Drivers::EInk::FULL, false);
delay(1000); // Cooldown. Back to back updates aren't great for health. delay(1000); // Cooldown. Back to back updates aren't great for health.
inverted = false; inverted = false;
inkhud->forceUpdate(Drivers::EInk::FULL, true, false); inkhud->forceUpdate(Drivers::EInk::FULL, false);
delay(1000); // Cooldown delay(1000); // Cooldown
// Prepare for the powered-off screen now // Prepare for the powered-off screen now
@@ -155,18 +155,6 @@ void InkHUD::LogoApplet::onShutdown()
// This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is complete // This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is complete
} }
void InkHUD::LogoApplet::onApplyingChanges()
{
bringToForeground();
textLeft = "";
textRight = "";
textTitle = "Applying changes";
fontTitle = fontSmall;
inkhud->forceUpdate(Drivers::EInk::FAST, false);
}
void InkHUD::LogoApplet::onReboot() void InkHUD::LogoApplet::onReboot()
{ {
bringToForeground(); bringToForeground();
@@ -176,7 +164,7 @@ void InkHUD::LogoApplet::onReboot()
textTitle = "Rebooting..."; textTitle = "Rebooting...";
fontTitle = fontSmall; fontTitle = fontSmall;
inkhud->forceUpdate(Drivers::EInk::FULL, true, false); inkhud->forceUpdate(Drivers::EInk::FULL, false);
// Perform the update right now, waiting here until complete // Perform the update right now, waiting here until complete
} }

View File

@@ -21,12 +21,11 @@ class LogoApplet : public SystemApplet, public concurrency::OSThread
{ {
public: public:
LogoApplet(); LogoApplet();
void onRender(bool full) override; void onRender() override;
void onForeground() override; void onForeground() override;
void onBackground() override; void onBackground() override;
void onShutdown() override; void onShutdown() override;
void onReboot() override; void onReboot() override;
void onApplyingChanges();
protected: protected:
int32_t runOnce() override; int32_t runOnce() override;

View File

@@ -19,7 +19,6 @@ namespace NicheGraphics::InkHUD
enum MenuAction { enum MenuAction {
NO_ACTION, NO_ACTION,
SEND_PING, SEND_PING,
FREE_TEXT,
STORE_CANNEDMESSAGE_SELECTION, STORE_CANNEDMESSAGE_SELECTION,
SEND_CANNEDMESSAGE, SEND_CANNEDMESSAGE,
SHUTDOWN, SHUTDOWN,
@@ -37,84 +36,6 @@ enum MenuAction {
TOGGLE_NOTIFICATIONS, TOGGLE_NOTIFICATIONS,
TOGGLE_INVERT_COLOR, TOGGLE_INVERT_COLOR,
TOGGLE_12H_CLOCK, TOGGLE_12H_CLOCK,
// Regions
SET_REGION_US,
SET_REGION_EU_868,
SET_REGION_EU_433,
SET_REGION_CN,
SET_REGION_JP,
SET_REGION_ANZ,
SET_REGION_KR,
SET_REGION_TW,
SET_REGION_RU,
SET_REGION_IN,
SET_REGION_NZ_865,
SET_REGION_TH,
SET_REGION_LORA_24,
SET_REGION_UA_433,
SET_REGION_UA_868,
SET_REGION_MY_433,
SET_REGION_MY_919,
SET_REGION_SG_923,
SET_REGION_PH_433,
SET_REGION_PH_868,
SET_REGION_PH_915,
SET_REGION_ANZ_433,
SET_REGION_KZ_433,
SET_REGION_KZ_863,
SET_REGION_NP_865,
SET_REGION_BR_902,
// Device Roles
SET_ROLE_CLIENT,
SET_ROLE_CLIENT_MUTE,
SET_ROLE_ROUTER,
SET_ROLE_REPEATER,
// Presets
SET_PRESET_LONG_SLOW,
SET_PRESET_LONG_MODERATE,
SET_PRESET_LONG_FAST,
SET_PRESET_MEDIUM_SLOW,
SET_PRESET_MEDIUM_FAST,
SET_PRESET_SHORT_SLOW,
SET_PRESET_SHORT_FAST,
SET_PRESET_SHORT_TURBO,
// Timezones
SET_TZ_US_HAWAII,
SET_TZ_US_ALASKA,
SET_TZ_US_PACIFIC,
SET_TZ_US_ARIZONA,
SET_TZ_US_MOUNTAIN,
SET_TZ_US_CENTRAL,
SET_TZ_US_EASTERN,
SET_TZ_BR_BRAZILIA,
SET_TZ_UTC,
SET_TZ_EU_WESTERN,
SET_TZ_EU_CENTRAL,
SET_TZ_EU_EASTERN,
SET_TZ_ASIA_KOLKATA,
SET_TZ_ASIA_HONG_KONG,
SET_TZ_AU_AWST,
SET_TZ_AU_ACST,
SET_TZ_AU_AEST,
SET_TZ_PACIFIC_NZ,
// Power
TOGGLE_POWER_SAVE,
CALIBRATE_ADC,
// Bluetooth
TOGGLE_BLUETOOTH,
TOGGLE_BLUETOOTH_PAIR_MODE,
// Channel
TOGGLE_CHANNEL_UPLINK,
TOGGLE_CHANNEL_DOWNLINK,
TOGGLE_CHANNEL_POSITION,
SET_CHANNEL_PRECISION,
// Display
TOGGLE_DISPLAY_UNITS,
// Network
TOGGLE_WIFI,
// Administration
RESET_NODEDB_ALL,
RESET_NODEDB_KEEP_FAVORITES,
}; };
} // namespace NicheGraphics::InkHUD } // namespace NicheGraphics::InkHUD

File diff suppressed because it is too large Load Diff

View File

@@ -32,13 +32,9 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
void onNavDown() override; void onNavDown() override;
void onNavLeft() override; void onNavLeft() override;
void onNavRight() override; void onNavRight() override;
void onFreeText(char c) override; void onRender() override;
void onFreeTextDone() override;
void onFreeTextCancel() override;
void onRender(bool full) override;
void show(Tile *t); // Open the menu, onto a user tile void show(Tile *t); // Open the menu, onto a user tile
void setStartPage(MenuPage page);
protected: protected:
Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton
@@ -54,32 +50,20 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height,
std::string text); // Draw input field for free text
uint16_t getSystemInfoPanelHeight(); uint16_t getSystemInfoPanelHeight();
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
uint16_t *height = nullptr); // Info panel at top of root menu uint16_t *height = nullptr); // Info panel at top of root menu
void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh
void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data
MenuPage startPageOverride = MenuPage::ROOT;
MenuPage currentPage = MenuPage::ROOT; MenuPage currentPage = MenuPage::ROOT;
MenuPage previousPage = MenuPage::EXIT; MenuPage previousPage = MenuPage::EXIT;
uint8_t cursor = 0; // Which menu item is currently highlighted uint8_t cursor = 0; // Which menu item is currently highlighted
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection) bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
bool freeTextMode = false;
uint16_t systemInfoPanelHeight = 0; // Need to know before we render uint16_t systemInfoPanelHeight = 0; // Need to know before we render
uint16_t menuTextLimit = 200;
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
std::vector<std::string> nodeConfigLabels; // Persistent labels for Node Config pages
uint8_t selectedChannelIndex = 0; // Currently selected LoRa channel (Node Config → Radio → Channel)
bool channelPositionEnabled = false;
bool gpsEnabled = false;
// Recents menu checkbox state (derived from settings.recentlyActiveSeconds)
static constexpr uint8_t RECENTS_COUNT = 6;
bool recentsSelected[RECENTS_COUNT] = {};
// Data for selecting and sending canned messages via the menu // Data for selecting and sending canned messages via the menu
// Placed into a sub-class for organization only // Placed into a sub-class for organization only
@@ -110,8 +94,6 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
// Cleared onBackground (when MenuApplet closes) // Cleared onBackground (when MenuApplet closes)
std::vector<MessageItem> messageItems; std::vector<MessageItem> messageItems;
std::vector<RecipientItem> recipientItems; std::vector<RecipientItem> recipientItems;
MessageItem freeTextItem;
} cm; } cm;
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu

View File

@@ -30,7 +30,6 @@ class MenuItem
MenuAction action = NO_ACTION; MenuAction action = NO_ACTION;
MenuPage nextPage = EXIT; MenuPage nextPage = EXIT;
bool *checkState = nullptr; bool *checkState = nullptr;
bool isHeader = false; // Non-selectable section label
// Various constructors, depending on the intended function of the item // Various constructors, depending on the intended function of the item
@@ -41,12 +40,6 @@ class MenuItem
: label(label), action(action), nextPage(nextPage), checkState(checkState) : label(label), action(action), nextPage(nextPage), checkState(checkState)
{ {
} }
static MenuItem Header(const char *label)
{
MenuItem item(label, NO_ACTION, EXIT);
item.isHeader = true;
return item;
}
}; };
} // namespace NicheGraphics::InkHUD } // namespace NicheGraphics::InkHUD

View File

@@ -20,27 +20,10 @@ enum MenuPage : uint8_t {
SEND, SEND,
CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message
OPTIONS, OPTIONS,
NODE_CONFIG,
NODE_CONFIG_LORA,
NODE_CONFIG_CHANNELS, // List of channels
NODE_CONFIG_CHANNEL_DETAIL, // Per-channel options
NODE_CONFIG_CHANNEL_PRECISION,
NODE_CONFIG_PRESET,
NODE_CONFIG_DEVICE,
NODE_CONFIG_DEVICE_ROLE,
NODE_CONFIG_POWER,
NODE_CONFIG_POWER_ADC_CAL,
NODE_CONFIG_NETWORK,
NODE_CONFIG_DISPLAY,
NODE_CONFIG_BLUETOOTH,
NODE_CONFIG_POSITION,
NODE_CONFIG_ADMIN_RESET,
TIMEZONE,
APPLETS, APPLETS,
AUTOSHOW, AUTOSHOW,
RECENTS, // Select length of "recentlyActiveSeconds" RECENTS, // Select length of "recentlyActiveSeconds"
REGION, EXIT, // Dismiss the menu applet
EXIT, // Dismiss the menu applet
}; };
} // namespace NicheGraphics::InkHUD } // namespace NicheGraphics::InkHUD

View File

@@ -65,7 +65,7 @@ int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket
return 0; return 0;
} }
void InkHUD::NotificationApplet::onRender(bool full) void InkHUD::NotificationApplet::onRender()
{ {
// Clear the region beneath the tile // Clear the region beneath the tile
// Most applets are drawing onto an empty frame buffer and don't need to do this // Most applets are drawing onto an empty frame buffer and don't need to do this
@@ -139,47 +139,54 @@ void InkHUD::NotificationApplet::onForeground()
void InkHUD::NotificationApplet::onBackground() void InkHUD::NotificationApplet::onBackground()
{ {
handleInput = false; handleInput = false;
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
} }
void InkHUD::NotificationApplet::onButtonShortPress() void InkHUD::NotificationApplet::onButtonShortPress()
{ {
dismiss(); dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
void InkHUD::NotificationApplet::onButtonLongPress() void InkHUD::NotificationApplet::onButtonLongPress()
{ {
dismiss(); dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
void InkHUD::NotificationApplet::onExitShort() void InkHUD::NotificationApplet::onExitShort()
{ {
dismiss(); dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
void InkHUD::NotificationApplet::onExitLong() void InkHUD::NotificationApplet::onExitLong()
{ {
dismiss(); dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
void InkHUD::NotificationApplet::onNavUp() void InkHUD::NotificationApplet::onNavUp()
{ {
dismiss(); dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
void InkHUD::NotificationApplet::onNavDown() void InkHUD::NotificationApplet::onNavDown()
{ {
dismiss(); dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
void InkHUD::NotificationApplet::onNavLeft() void InkHUD::NotificationApplet::onNavLeft()
{ {
dismiss(); dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
void InkHUD::NotificationApplet::onNavRight() void InkHUD::NotificationApplet::onNavRight()
{ {
dismiss(); dismiss();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
// Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification // Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification

View File

@@ -26,7 +26,7 @@ class NotificationApplet : public SystemApplet
public: public:
NotificationApplet(); NotificationApplet();
void onRender(bool full) override; void onRender() override;
void onForeground() override; void onForeground() override;
void onBackground() override; void onBackground() override;
void onButtonShortPress() override; void onButtonShortPress() override;

View File

@@ -9,7 +9,7 @@ InkHUD::PairingApplet::PairingApplet()
bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus);
} }
void InkHUD::PairingApplet::onRender(bool full) void InkHUD::PairingApplet::onRender()
{ {
// Header // Header
setFont(fontMedium); setFont(fontMedium);
@@ -45,7 +45,7 @@ void InkHUD::PairingApplet::onBackground()
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status) int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status)

View File

@@ -22,7 +22,7 @@ class PairingApplet : public SystemApplet
public: public:
PairingApplet(); PairingApplet();
void onRender(bool full) override; void onRender() override;
void onForeground() override; void onForeground() override;
void onBackground() override; void onBackground() override;

View File

@@ -4,7 +4,7 @@
using namespace NicheGraphics; using namespace NicheGraphics;
void InkHUD::PlaceholderApplet::onRender(bool full) void InkHUD::PlaceholderApplet::onRender()
{ {
// This placeholder applet fills its area with sparse diagonal lines // This placeholder applet fills its area with sparse diagonal lines
hatchRegion(0, 0, width(), height(), 8, BLACK); hatchRegion(0, 0, width(), height(), 8, BLACK);

View File

@@ -17,7 +17,7 @@ namespace NicheGraphics::InkHUD
class PlaceholderApplet : public SystemApplet class PlaceholderApplet : public SystemApplet
{ {
public: public:
void onRender(bool full) override; void onRender() override;
// Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet. // Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet.
// The window manager decides when and where it should be rendered // The window manager decides when and where it should be rendered

View File

@@ -10,42 +10,39 @@ using namespace NicheGraphics;
InkHUD::TipsApplet::TipsApplet() InkHUD::TipsApplet::TipsApplet()
{ {
bool needsRegion = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET); // Decide which tips (if any) should be shown to user after the boot screen
bool showTutorialTips = (settings->tips.firstBoot || needsRegion);
// Welcome screen // Welcome screen
if (showTutorialTips) if (settings->tips.firstBoot)
tipQueue.push_back(Tip::WELCOME); tipQueue.push_back(Tip::WELCOME);
// Finish setup // Antenna, region, timezone
if (needsRegion) // Shown at boot if region not yet set
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
tipQueue.push_back(Tip::FINISH_SETUP); tipQueue.push_back(Tip::FINISH_SETUP);
// Using the UI
if (showTutorialTips) {
tipQueue.push_back(Tip::CUSTOMIZATION);
tipQueue.push_back(Tip::BUTTONS);
}
// Shutdown info // Shutdown info
// Shown until user performs one valid shutdown // Shown until user performs one valid shutdown
if (!settings->tips.safeShutdownSeen) if (!settings->tips.safeShutdownSeen)
tipQueue.push_back(Tip::SAFE_SHUTDOWN); tipQueue.push_back(Tip::SAFE_SHUTDOWN);
// Using the UI
if (settings->tips.firstBoot) {
tipQueue.push_back(Tip::CUSTOMIZATION);
tipQueue.push_back(Tip::BUTTONS);
}
// Catch an incorrect attempt at rotating display // Catch an incorrect attempt at rotating display
if (config.display.flip_screen) if (config.display.flip_screen)
tipQueue.push_back(Tip::ROTATION); tipQueue.push_back(Tip::ROTATION);
// Region picker // Applet is foreground immediately at boot, but is obscured by LogoApplet, which is also foreground
if (needsRegion) // LogoApplet can be considered to have a higher Z-index, because it is placed before TipsApplet in the systemApplets vector
tipQueue.push_back(Tip::PICK_REGION);
if (!tipQueue.empty()) if (!tipQueue.empty())
bringToForeground(); bringToForeground();
} }
void InkHUD::TipsApplet::onRender(bool full) void InkHUD::TipsApplet::onRender()
{ {
switch (tipQueue.front()) { switch (tipQueue.front()) {
case Tip::WELCOME: case Tip::WELCOME:
@@ -54,109 +51,81 @@ void InkHUD::TipsApplet::onRender(bool full)
case Tip::FINISH_SETUP: { case Tip::FINISH_SETUP: {
setFont(fontMedium); setFont(fontMedium);
const char *title = "Tip: Finish Setup"; printAt(0, 0, "Tip: Finish Setup");
uint16_t h = getWrappedTextHeight(0, width(), title);
printWrapped(0, 0, width(), title);
setFont(fontSmall); setFont(fontSmall);
int16_t cursorY = h + fontSmall.lineHeight(); int16_t cursorY = fontMedium.lineHeight() * 1.5;
printAt(0, cursorY, "- connect antenna");
auto drawBullet = [&](const char *text) { cursorY += fontSmall.lineHeight() * 1.2;
uint16_t bh = getWrappedTextHeight(0, width(), text); printAt(0, cursorY, "- connect a client app");
printWrapped(0, cursorY, width(), text);
cursorY += bh + (fontSmall.lineHeight() / 3);
};
drawBullet("- connect antenna"); // Only if region not set
drawBullet("- connect a client app"); if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
cursorY += fontSmall.lineHeight() * 1.2;
printAt(0, cursorY, "- set region");
}
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) // Only if tz not set
drawBullet("- set region"); if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) {
cursorY += fontSmall.lineHeight() * 1.2;
printAt(0, cursorY, "- set timezone");
}
if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) cursorY += fontSmall.lineHeight() * 1.5;
drawBullet("- set timezone"); printAt(0, cursorY, "More info at meshtastic.org");
cursorY += fontSmall.lineHeight() / 2;
drawBullet("More info at meshtastic.org");
setFont(fontSmall);
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
} break; } break;
case Tip::PICK_REGION: {
setFont(fontMedium);
printAt(0, 0, "Set Region");
setFont(fontSmall);
printWrapped(0, fontMedium.lineHeight() * 1.5, width(), "Please select your LoRa region to complete setup.");
printAt(0, Y(1.0), "Press button to choose", LEFT, BOTTOM);
} break;
case Tip::SAFE_SHUTDOWN: { case Tip::SAFE_SHUTDOWN: {
setFont(fontMedium); setFont(fontMedium);
printAt(0, 0, "Tip: Shutdown");
const char *title = "Tip: Shutdown";
uint16_t h = getWrappedTextHeight(0, width(), title);
printWrapped(0, 0, width(), title);
setFont(fontSmall); setFont(fontSmall);
int16_t cursorY = h + fontSmall.lineHeight(); std::string shutdown;
shutdown += "Before removing power, please shut down from InkHUD menu, or a client app. \n";
const char *body = "Before removing power, please shut down from InkHUD menu, or a client app.\n\n" shutdown += "\n";
"This ensures data is saved."; shutdown += "This ensures data is saved.";
printWrapped(0, fontMedium.lineHeight() * 1.5, width(), shutdown);
uint16_t bodyH = getWrappedTextHeight(0, width(), body);
printWrapped(0, cursorY, width(), body);
cursorY += bodyH + (fontSmall.lineHeight() / 2);
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
} break; } break;
case Tip::CUSTOMIZATION: { case Tip::CUSTOMIZATION: {
setFont(fontMedium); setFont(fontMedium);
printAt(0, 0, "Tip: Customization");
const char *title = "Tip: Customization";
uint16_t h = getWrappedTextHeight(0, width(), title);
printWrapped(0, 0, width(), title);
setFont(fontSmall); setFont(fontSmall);
int16_t cursorY = h + fontSmall.lineHeight(); printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
"Configure & control display with the InkHUD menu. Optional features, layout, rotation, and more.");
const char *body = "Configure & control display with the InkHUD menu. "
"Optional features, layout, rotation, and more.";
uint16_t bodyH = getWrappedTextHeight(0, width(), body);
printWrapped(0, cursorY, width(), body);
cursorY += bodyH + (fontSmall.lineHeight() / 2);
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
} break; } break;
case Tip::BUTTONS: { case Tip::BUTTONS: {
setFont(fontMedium); setFont(fontMedium);
printAt(0, 0, "Tip: Buttons");
const char *title = "Tip: Buttons";
uint16_t h = getWrappedTextHeight(0, width(), title);
printWrapped(0, 0, width(), title);
setFont(fontSmall); setFont(fontSmall);
int16_t cursorY = h + fontSmall.lineHeight(); int16_t cursorY = fontMedium.lineHeight() * 1.5;
auto drawBullet = [&](const char *text) {
uint16_t bh = getWrappedTextHeight(0, width(), text);
printWrapped(0, cursorY, width(), text);
cursorY += bh + (fontSmall.lineHeight() / 3);
};
if (!settings->joystick.enabled) { if (!settings->joystick.enabled) {
drawBullet("User Button"); printAt(0, cursorY, "User Button");
drawBullet("- short press: next"); cursorY += fontSmall.lineHeight() * 1.2;
drawBullet("- long press: select or open menu"); printAt(0, cursorY, "- short press: next");
cursorY += fontSmall.lineHeight() * 1.2;
printAt(0, cursorY, "- long press: select / open menu");
} else { } else {
drawBullet("Joystick"); printAt(0, cursorY, "Joystick");
drawBullet("- press: open menu or select"); cursorY += fontSmall.lineHeight() * 1.2;
drawBullet("Exit Button"); printAt(0, cursorY, "- open menu / select");
drawBullet("- press: switch tile or close menu"); cursorY += fontSmall.lineHeight() * 1.5;
printAt(0, cursorY, "Exit Button");
cursorY += fontSmall.lineHeight() * 1.2;
printAt(0, cursorY, "- switch tile / close menu");
} }
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
@@ -164,21 +133,12 @@ void InkHUD::TipsApplet::onRender(bool full)
case Tip::ROTATION: { case Tip::ROTATION: {
setFont(fontMedium); setFont(fontMedium);
printAt(0, 0, "Tip: Rotation");
const char *title = "Tip: Rotation";
uint16_t h = getWrappedTextHeight(0, width(), title);
printWrapped(0, 0, width(), title);
setFont(fontSmall); setFont(fontSmall);
if (!settings->joystick.enabled) { if (!settings->joystick.enabled) {
int16_t cursorY = h + fontSmall.lineHeight(); printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
"To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate.");
const char *body = "To rotate the display, use the InkHUD menu. "
"Long-press the user button > Options > Rotate.";
uint16_t bh = getWrappedTextHeight(0, width(), body);
printWrapped(0, cursorY, width(), body);
cursorY += bh + (fontSmall.lineHeight() / 2);
} else { } else {
printWrapped(0, fontMedium.lineHeight() * 1.5, width(), printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
"To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate."); "To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate.");
@@ -199,15 +159,12 @@ void InkHUD::TipsApplet::renderWelcome()
{ {
uint16_t padW = X(0.05); uint16_t padW = X(0.05);
// Detect portrait orientation
bool portrait = height() > width();
// Block 1 - logo & title // Block 1 - logo & title
// ======================== // ========================
// Logo size // Logo size
uint16_t logoWLimit = portrait ? X(0.5) : X(0.3); uint16_t logoWLimit = X(0.3);
uint16_t logoHLimit = portrait ? Y(0.25) : Y(0.3); uint16_t logoHLimit = Y(0.3);
uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit); uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit);
uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit);
@@ -220,7 +177,7 @@ void InkHUD::TipsApplet::renderWelcome()
// Center the block // Center the block
// Desired effect: equal margin from display edge for logo left and title right // Desired effect: equal margin from display edge for logo left and title right
int16_t block1Y = portrait ? Y(0.2) : Y(0.3); int16_t block1Y = Y(0.3);
int16_t block1CX = X(0.5) + (logoW / 2) - (titleW / 2); int16_t block1CX = X(0.5) + (logoW / 2) - (titleW / 2);
int16_t logoCX = block1CX - (logoW / 2) - (padW / 2); int16_t logoCX = block1CX - (logoW / 2) - (padW / 2);
int16_t titleCX = block1CX + (titleW / 2) + (padW / 2); int16_t titleCX = block1CX + (titleW / 2) + (padW / 2);
@@ -235,7 +192,7 @@ void InkHUD::TipsApplet::renderWelcome()
std::string subtitle = "InkHUD"; std::string subtitle = "InkHUD";
if (width() >= 200) if (width() >= 200)
subtitle += " - A Heads-Up Display"; // Future proofing: narrower for tiny display subtitle += " - A Heads-Up Display"; // Future proofing: narrower for tiny display
printAt(X(0.5), portrait ? Y(0.45) : Y(0.6), subtitle, CENTER, MIDDLE); printAt(X(0.5), Y(0.6), subtitle, CENTER, MIDDLE);
// Block 3 - press to continue // Block 3 - press to continue
// ============================ // ============================
@@ -261,42 +218,32 @@ void InkHUD::TipsApplet::onBackground()
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} }
// While our SystemApplet::handleInput flag is true // While our SystemApplet::handleInput flag is true
void InkHUD::TipsApplet::onButtonShortPress() void InkHUD::TipsApplet::onButtonShortPress()
{ {
bool needsRegion = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET);
// If we're prompting the user to pick a region, hand off to the menu
if (!tipQueue.empty() && tipQueue.front() == Tip::PICK_REGION) {
tipQueue.pop_front();
// Signal InkHUD to open the menu on Region page
inkhud->forceRegionMenu = true;
// Close tips and open menu
sendToBackground();
inkhud->openMenu();
return;
}
// Consume current tip
tipQueue.pop_front(); tipQueue.pop_front();
// All tips done // All tips done
if (tipQueue.empty()) { if (tipQueue.empty()) {
// Record that user has now seen the "tutorial" set of tips // Record that user has now seen the "tutorial" set of tips
// Don't show them on subsequent boots // Don't show them on subsequent boots
if (settings->tips.firstBoot && !needsRegion) { if (settings->tips.firstBoot) {
settings->tips.firstBoot = false; settings->tips.firstBoot = false;
inkhud->persistence->saveSettings(); inkhud->persistence->saveSettings();
} }
// Close applet // Close applet, and full refresh to clean the screen
// Need to force update, because our request would be ignored otherwise, as we are now background
sendToBackground(); sendToBackground();
} else { inkhud->forceUpdate(EInk::UpdateTypes::FULL);
requestUpdate();
} }
// More tips left
else
requestUpdate();
} }
// Functions the same as the user button in this instance // Functions the same as the user button in this instance
@@ -305,4 +252,4 @@ void InkHUD::TipsApplet::onExitShort()
onButtonShortPress(); onButtonShortPress();
} }
#endif #endif

View File

@@ -23,7 +23,6 @@ class TipsApplet : public SystemApplet
enum class Tip { enum class Tip {
WELCOME, WELCOME,
FINISH_SETUP, FINISH_SETUP,
PICK_REGION,
SAFE_SHUTDOWN, SAFE_SHUTDOWN,
CUSTOMIZATION, CUSTOMIZATION,
BUTTONS, BUTTONS,
@@ -33,7 +32,7 @@ class TipsApplet : public SystemApplet
public: public:
TipsApplet(); TipsApplet();
void onRender(bool full) override; void onRender() override;
void onForeground() override; void onForeground() override;
void onBackground() override; void onBackground() override;
void onButtonShortPress() override; void onButtonShortPress() override;

View File

@@ -34,7 +34,7 @@ int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *
return 0; return 0;
} }
void InkHUD::AllMessageApplet::onRender(bool full) void InkHUD::AllMessageApplet::onRender()
{ {
// Find newest message, regardless of whether DM or broadcast // Find newest message, regardless of whether DM or broadcast
MessageStore::Message *message; MessageStore::Message *message;

View File

@@ -30,7 +30,7 @@ class Applet;
class AllMessageApplet : public Applet class AllMessageApplet : public Applet
{ {
public: public:
void onRender(bool full) override; void onRender() override;
void onActivate() override; void onActivate() override;
void onDeactivate() override; void onDeactivate() override;

View File

@@ -37,7 +37,7 @@ int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
return 0; return 0;
} }
void InkHUD::DMApplet::onRender(bool full) void InkHUD::DMApplet::onRender()
{ {
// Abort if no text message // Abort if no text message
if (!latestMessage->dm.sender) { if (!latestMessage->dm.sender) {

View File

@@ -30,7 +30,7 @@ class Applet;
class DMApplet : public Applet class DMApplet : public Applet
{ {
public: public:
void onRender(bool full) override; void onRender() override;
void onActivate() override; void onActivate() override;
void onDeactivate() override; void onDeactivate() override;

View File

@@ -5,10 +5,10 @@
using namespace NicheGraphics; using namespace NicheGraphics;
void InkHUD::PositionsApplet::onRender(bool full) void InkHUD::PositionsApplet::onRender()
{ {
// Draw the usual map applet first // Draw the usual map applet first
MapApplet::onRender(full); MapApplet::onRender();
// Draw our latest "node of interest" as a special marker // Draw our latest "node of interest" as a special marker
// ------------------------------------------------------- // -------------------------------------------------------

View File

@@ -24,7 +24,7 @@ class PositionsApplet : public MapApplet, public SinglePortModule
{ {
public: public:
PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {} PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {}
void onRender(bool full) override; void onRender() override;
protected: protected:
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;

View File

@@ -22,7 +22,7 @@ InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex)
store = new MessageStore("ch" + to_string(channelIndex)); store = new MessageStore("ch" + to_string(channelIndex));
} }
void InkHUD::ThreadedMessageApplet::onRender(bool full) void InkHUD::ThreadedMessageApplet::onRender()
{ {
// ============= // =============
// Draw a header // Draw a header

View File

@@ -36,7 +36,7 @@ class ThreadedMessageApplet : public Applet, public SinglePortModule
explicit ThreadedMessageApplet(uint8_t channelIndex); explicit ThreadedMessageApplet(uint8_t channelIndex);
ThreadedMessageApplet() = delete; ThreadedMessageApplet() = delete;
void onRender(bool full) override; void onRender() override;
void onActivate() override; void onActivate() override;
void onDeactivate() override; void onDeactivate() override;

View File

@@ -238,39 +238,6 @@ void InkHUD::Events::onNavRight()
} }
} }
void InkHUD::Events::onFreeText(char c)
{
// Trigger the first system applet that wants to handle the new character
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleFreeText) {
sa->onFreeText(c);
break;
}
}
}
void InkHUD::Events::onFreeTextDone()
{
// Trigger the first system applet that wants to handle it
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleFreeText) {
sa->onFreeTextDone();
break;
}
}
}
void InkHUD::Events::onFreeTextCancel()
{
// Trigger the first system applet that wants to handle it
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleFreeText) {
sa->onFreeTextCancel();
break;
}
}
}
// Callback for deepSleepObserver // Callback for deepSleepObserver
// Returns 0 to signal that we agree to sleep now // Returns 0 to signal that we agree to sleep now
int InkHUD::Events::beforeDeepSleep(void *unused) int InkHUD::Events::beforeDeepSleep(void *unused)
@@ -299,7 +266,7 @@ int InkHUD::Events::beforeDeepSleep(void *unused)
// then prepared a final powered-off screen for us, which shows device shortname. // then prepared a final powered-off screen for us, which shows device shortname.
// We're updating to show that one now. // We're updating to show that one now.
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false); inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
delay(1000); // Cooldown, before potentially yanking display power delay(1000); // Cooldown, before potentially yanking display power
// InkHUD shutdown complete // InkHUD shutdown complete
@@ -309,15 +276,6 @@ int InkHUD::Events::beforeDeepSleep(void *unused)
return 0; // We agree: deep sleep now return 0; // We agree: deep sleep now
} }
// Display an intermediate screen while configuration changes are applied
void InkHUD::Events::applyingChanges()
{
// Bring the logo applet forward with a temporary message
for (SystemApplet *sa : inkhud->systemApplets) {
sa->onApplyingChanges();
}
}
// Callback for rebootObserver // Callback for rebootObserver
// Same as shutdown, without drawing the logoApplet // Same as shutdown, without drawing the logoApplet
// Makes sure we don't lose message history / InkHUD config // Makes sure we don't lose message history / InkHUD config

View File

@@ -29,18 +29,12 @@ class Events
void onButtonShort(); // User button: short press void onButtonShort(); // User button: short press
void onButtonLong(); // User button: long press void onButtonLong(); // User button: long press
void applyingChanges(); void onExitShort(); // Exit button: short press
void onExitShort(); // Exit button: short press void onExitLong(); // Exit button: long press
void onExitLong(); // Exit button: long press void onNavUp(); // Navigate up
void onNavUp(); // Navigate up void onNavDown(); // Navigate down
void onNavDown(); // Navigate down void onNavLeft(); // Navigate left
void onNavLeft(); // Navigate left void onNavRight(); // Navigate right
void onNavRight(); // Navigate right
// Free text typing events
void onFreeText(char c); // New freetext character input
void onFreeTextDone();
void onFreeTextCancel();
int beforeDeepSleep(void *unused); // Prepare for shutdown int beforeDeepSleep(void *unused); // Prepare for shutdown
int beforeReboot(void *unused); // Prepare for reboot int beforeReboot(void *unused); // Prepare for reboot

View File

@@ -53,13 +53,6 @@ void InkHUD::InkHUD::addApplet(const char *name, Applet *a, bool defaultActive,
windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile); windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile);
} }
void InkHUD::InkHUD::notifyApplyingChanges()
{
if (events) {
events->applyingChanges();
}
}
// Start InkHUD! // Start InkHUD!
// Call this only after you have configured InkHUD // Call this only after you have configured InkHUD
void InkHUD::InkHUD::begin() void InkHUD::InkHUD::begin()
@@ -175,25 +168,6 @@ void InkHUD::InkHUD::navRight()
} }
} }
// Call this for keyboard input
// The Keyboard Applet also calls this
void InkHUD::InkHUD::freeText(char c)
{
events->onFreeText(c);
}
// Call this to complete a freetext input
void InkHUD::InkHUD::freeTextDone()
{
events->onFreeTextDone();
}
// Call this to cancel a freetext input
void InkHUD::InkHUD::freeTextCancel()
{
events->onFreeTextCancel();
}
// Cycle the next user applet to the foreground // Cycle the next user applet to the foreground
// Only activated applets are cycled // Only activated applets are cycled
// If user has a multi-applet layout, the applets will cycle on the "focused tile" // If user has a multi-applet layout, the applets will cycle on the "focused tile"
@@ -223,18 +197,6 @@ void InkHUD::InkHUD::openAlignStick()
windowManager->openAlignStick(); windowManager->openAlignStick();
} }
// Open the on-screen keyboard
void InkHUD::InkHUD::openKeyboard()
{
windowManager->openKeyboard();
}
// Close the on-screen keyboard
void InkHUD::InkHUD::closeKeyboard()
{
windowManager->closeKeyboard();
}
// In layouts where multiple applets are shown at once, change which tile is focused // In layouts where multiple applets are shown at once, change which tile is focused
// The focused tile in the one which cycles applets on button short press, and displays menu on long press // The focused tile in the one which cycles applets on button short press, and displays menu on long press
void InkHUD::InkHUD::nextTile() void InkHUD::InkHUD::nextTile()
@@ -283,11 +245,10 @@ void InkHUD::InkHUD::requestUpdate()
// Ignores all diplomacy: // Ignores all diplomacy:
// - the display *will* update // - the display *will* update
// - the specified update type *will* be used // - the specified update type *will* be used
// If the all parameter is true, the whole screen buffer is cleared and re-rendered
// If the async parameter is false, code flow is blocked while the update takes place // If the async parameter is false, code flow is blocked while the update takes place
void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool all, bool async) void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool async)
{ {
renderer->forceUpdate(type, all, async); renderer->forceUpdate(type, async);
} }
// Wait for any in-progress display update to complete before continuing // Wait for any in-progress display update to complete before continuing

View File

@@ -47,7 +47,6 @@ class InkHUD
void setDriver(Drivers::EInk *driver); void setDriver(Drivers::EInk *driver);
void setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0); void setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0);
void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, uint8_t onTile = -1); void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, uint8_t onTile = -1);
void notifyApplyingChanges();
void begin(); void begin();
@@ -63,11 +62,6 @@ class InkHUD
void navLeft(); void navLeft();
void navRight(); void navRight();
// Freetext handlers
void freeText(char c);
void freeTextDone();
void freeTextCancel();
// Trigger UI changes // Trigger UI changes
// - called by various InkHUD components // - called by various InkHUD components
// - suitable(?) for use by aux button, connected in variant nicheGraphics.h // - suitable(?) for use by aux button, connected in variant nicheGraphics.h
@@ -76,23 +70,17 @@ class InkHUD
void prevApplet(); void prevApplet();
void openMenu(); void openMenu();
void openAlignStick(); void openAlignStick();
void openKeyboard();
void closeKeyboard();
void nextTile(); void nextTile();
void prevTile(); void prevTile();
void rotate(); void rotate();
void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default
void toggleBatteryIcon(); void toggleBatteryIcon();
// Used by TipsApplet to force menu to start on Region selection
bool forceRegionMenu = false;
// Updating the display // Updating the display
// - called by various InkHUD components // - called by various InkHUD components
void requestUpdate(); void requestUpdate();
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool all = false, void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool async = true);
bool async = true);
void awaitUpdate(); void awaitUpdate();
// (Re)configuring WindowManager // (Re)configuring WindowManager

View File

@@ -56,16 +56,15 @@ void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMul
void InkHUD::Renderer::begin() void InkHUD::Renderer::begin()
{ {
forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false); forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
} }
// Set a flag, which will be picked up by runOnce, ASAP. // Set a flag, which will be picked up by runOnce, ASAP.
// Quite likely, multiple applets will all want to respond to one event (Observable, etc) // Quite likely, multiple applets will all want to respond to one event (Observable, etc)
// Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next runOnce // Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next runOnce
void InkHUD::Renderer::requestUpdate(bool all) void InkHUD::Renderer::requestUpdate()
{ {
requested = true; requested = true;
renderAll |= all;
// We will run the thread as soon as we loop(), // We will run the thread as soon as we loop(),
// after all Applets have had a chance to observe whatever event set this off // after all Applets have had a chance to observe whatever event set this off
@@ -80,11 +79,10 @@ void InkHUD::Renderer::requestUpdate(bool all)
// Sometimes, however, we will want to trigger a display update manually, in the absence of any sort of applet event // Sometimes, however, we will want to trigger a display update manually, in the absence of any sort of applet event
// Display health, for example. // Display health, for example.
// In these situations, we use forceUpdate // In these situations, we use forceUpdate
void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool all, bool async) void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool async)
{ {
requested = true; requested = true;
forced = true; forced = true;
renderAll |= all;
displayHealth.forceUpdateType(type); displayHealth.forceUpdateType(type);
// Normally, we need to start the timer, in case the display is busy and we briefly defer the update // Normally, we need to start the timer, in case the display is busy and we briefly defer the update
@@ -221,8 +219,7 @@ void InkHUD::Renderer::render(bool async)
Drivers::EInk::UpdateTypes updateType = decideUpdateType(); Drivers::EInk::UpdateTypes updateType = decideUpdateType();
// Render the new image // Render the new image
if (renderAll) clearBuffer();
clearBuffer();
renderUserApplets(); renderUserApplets();
renderPlaceholders(); renderPlaceholders();
renderSystemApplets(); renderSystemApplets();
@@ -250,7 +247,6 @@ void InkHUD::Renderer::render(bool async)
// Tidy up, ready for a new request // Tidy up, ready for a new request
requested = false; requested = false;
forced = false; forced = false;
renderAll = false;
} }
// Manually fill the image buffer with WHITE // Manually fill the image buffer with WHITE
@@ -263,76 +259,6 @@ void InkHUD::Renderer::clearBuffer()
memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth); memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth);
} }
// Manually clear the pixels below a tile
void InkHUD::Renderer::clearTile(Tile *t)
{
// Rotate the tile dimensions
int16_t left = 0;
int16_t top = 0;
uint16_t width = 0;
uint16_t height = 0;
switch (settings->rotation) {
case 0:
left = t->getLeft();
top = t->getTop();
width = t->getWidth();
height = t->getHeight();
break;
case 1:
left = driver->width - (t->getTop() + t->getHeight());
top = t->getLeft();
width = t->getHeight();
height = t->getWidth();
break;
case 2:
left = driver->width - (t->getLeft() + t->getWidth());
top = driver->height - (t->getTop() + t->getHeight());
width = t->getWidth();
height = t->getHeight();
break;
case 3:
left = t->getTop();
top = driver->height - (t->getLeft() + t->getWidth());
width = t->getHeight();
height = t->getWidth();
break;
}
// Calculate the bounds to clear
uint16_t xStart = (left < 0) ? 0 : left;
uint16_t yStart = (top < 0) ? 0 : top;
if (xStart >= driver->width || yStart >= driver->height || left + width < 0 || top + height < 0)
return; // the box is completely off the screen
uint16_t xEnd = left + width;
uint16_t yEnd = top + height;
if (xEnd > driver->width)
xEnd = driver->width;
if (yEnd > driver->height)
yEnd = driver->height;
// Clear the pixels
if (xStart == 0 && xEnd == driver->width) { // full width box is easier to clear
memset(imageBuffer + (yStart * imageBufferWidth), 0xFF, (yEnd - yStart) * imageBufferWidth);
} else {
const uint16_t byteStart = (xStart / 8) + 1;
const uint16_t byteEnd = xEnd / 8;
const uint8_t leadingByte = 0xFF >> (xStart - ((byteStart - 1) * 8));
const uint8_t trailingByte = (0xFF00 >> (xEnd - (byteEnd * 8))) & 0xFF;
for (uint16_t i = yStart * imageBufferWidth; i < yEnd * imageBufferWidth; i += imageBufferWidth) {
// Set the leading byte
imageBuffer[i + byteStart - 1] |= leadingByte;
// Set the continuous bytes
if (byteStart < byteEnd)
memset(imageBuffer + i + byteStart, 0xFF, byteEnd - byteStart);
// Set the trailing byte
if (byteEnd != imageBufferWidth)
imageBuffer[i + byteEnd] |= trailingByte;
}
}
}
void InkHUD::Renderer::checkLocks() void InkHUD::Renderer::checkLocks()
{ {
lockRendering = nullptr; lockRendering = nullptr;
@@ -397,12 +323,12 @@ Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType()
if (!forced) { if (!forced) {
// User applets // User applets
for (Applet *ua : inkhud->userApplets) { for (Applet *ua : inkhud->userApplets) {
if (ua && ua->isForeground() && (ua->wantsToRender() || renderAll)) if (ua && ua->isForeground())
displayHealth.requestUpdateType(ua->wantsUpdateType()); displayHealth.requestUpdateType(ua->wantsUpdateType());
} }
// System Applets // System Applets
for (SystemApplet *sa : inkhud->systemApplets) { for (SystemApplet *sa : inkhud->systemApplets) {
if (sa && sa->isForeground() && (sa->wantsToRender() || sa->alwaysRender || renderAll)) if (sa && sa->isForeground())
displayHealth.requestUpdateType(sa->wantsUpdateType()); displayHealth.requestUpdateType(sa->wantsUpdateType());
} }
} }
@@ -420,16 +346,9 @@ void InkHUD::Renderer::renderUserApplets()
// Render any user applets which are currently visible // Render any user applets which are currently visible
for (Applet *ua : inkhud->userApplets) { for (Applet *ua : inkhud->userApplets) {
if (ua && ua->isActive() && ua->isForeground() && (ua->wantsToRender() || renderAll)) { if (ua && ua->isActive() && ua->isForeground()) {
// Clear the tile unless the applet wants to draw over its previous render
// or everything is getting re-rendered anyways
if (ua->wantsFullRender() && !renderAll)
clearTile(ua->getTile());
uint32_t start = millis(); uint32_t start = millis();
bool full = ua->wantsFullRender() || renderAll; ua->render(); // Draw!
ua->render(full); // Draw!
uint32_t stop = millis(); uint32_t stop = millis();
LOG_DEBUG("%s took %dms to render", ua->name, stop - start); LOG_DEBUG("%s took %dms to render", ua->name, stop - start);
} }
@@ -451,9 +370,6 @@ void InkHUD::Renderer::renderSystemApplets()
if (!sa->isForeground()) if (!sa->isForeground())
continue; continue;
if (!sa->wantsToRender() && !sa->alwaysRender && !renderAll)
continue;
// Skip if locked by another applet // Skip if locked by another applet
if (lockRendering && lockRendering != sa) if (lockRendering && lockRendering != sa)
continue; continue;
@@ -465,14 +381,8 @@ void InkHUD::Renderer::renderSystemApplets()
assert(sa->getTile()); assert(sa->getTile());
// Clear the tile unless the applet wants to draw over its previous render
// or everything is getting re-rendered anyways
if (sa->wantsFullRender() && !renderAll)
clearTile(sa->getTile());
// uint32_t start = millis(); // uint32_t start = millis();
bool full = sa->wantsFullRender() || renderAll; sa->render(); // Draw!
sa->render(full); // Draw!
// uint32_t stop = millis(); // uint32_t stop = millis();
// LOG_DEBUG("%s took %dms to render", sa->name, stop - start); // LOG_DEBUG("%s took %dms to render", sa->name, stop - start);
} }
@@ -499,10 +409,7 @@ void InkHUD::Renderer::renderPlaceholders()
// uint32_t start = millis(); // uint32_t start = millis();
for (Tile *t : emptyTiles) { for (Tile *t : emptyTiles) {
t->assignApplet(placeholder); t->assignApplet(placeholder);
// Clear the tile unless everything is getting re-rendered placeholder->render();
if (!renderAll)
clearTile(t);
placeholder->render(true); // full render
t->assignApplet(nullptr); t->assignApplet(nullptr);
} }
// uint32_t stop = millis(); // uint32_t stop = millis();

View File

@@ -37,8 +37,8 @@ class Renderer : protected concurrency::OSThread
// Call these to make the image change // Call these to make the image change
void requestUpdate(bool all = false); // Update display, if a foreground applet has info it wants to show void requestUpdate(); // 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, void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED,
bool async = true); // Update display, regardless of whether any applets requested this bool async = true); // Update display, regardless of whether any applets requested this
// Wait for an update to complete // Wait for an update to complete
@@ -65,7 +65,6 @@ class Renderer : protected concurrency::OSThread
// Steps of the rendering process // Steps of the rendering process
void clearBuffer(); void clearBuffer();
void clearTile(Tile *t);
void checkLocks(); void checkLocks();
bool shouldUpdate(); bool shouldUpdate();
Drivers::EInk::UpdateTypes decideUpdateType(); Drivers::EInk::UpdateTypes decideUpdateType();
@@ -86,7 +85,6 @@ class Renderer : protected concurrency::OSThread
bool requested = false; bool requested = false;
bool forced = false; bool forced = false;
bool renderAll = false;
// For convenience // For convenience
InkHUD *inkhud = nullptr; InkHUD *inkhud = nullptr;

View File

@@ -22,14 +22,11 @@ class SystemApplet : public Applet
public: public:
// System applets have the right to: // System applets have the right to:
bool handleInput = false; // - respond to input from the user button bool handleInput = false; // - respond to input from the user button
bool handleFreeText = false; // - respond to free text input bool lockRendering = false; // - prevent other applets from being rendered during an update
bool lockRendering = false; // - prevent other applets from being rendered during an update bool lockRequests = false; // - prevent other applets from triggering display updates
bool lockRequests = false; // - prevent other applets from triggering display updates
bool alwaysRender = false; // - render every time the screen is updated
virtual void onReboot() { onShutdown(); } // - handle reboot specially virtual void onReboot() { onShutdown(); } // - handle reboot specially
virtual void onApplyingChanges() {}
// Other system applets may take precedence over our own system applet though // Other system applets may take precedence over our own system applet though
// The order an applet is passed to WindowManager::addSystemApplet determines this hierarchy (added earlier = higher rank) // The order an applet is passed to WindowManager::addSystemApplet determines this hierarchy (added earlier = higher rank)
@@ -43,4 +40,4 @@ class SystemApplet : public Applet
}; // namespace NicheGraphics::InkHUD }; // namespace NicheGraphics::InkHUD
#endif #endif

View File

@@ -18,7 +18,7 @@ static int32_t runtaskHighlight()
LOG_DEBUG("Dismissing Highlight"); LOG_DEBUG("Dismissing Highlight");
InkHUD::Tile::highlightShown = false; InkHUD::Tile::highlightShown = false;
InkHUD::Tile::highlightTarget = nullptr; InkHUD::Tile::highlightTarget = nullptr;
InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true); // Re-render, clearing the highlighting InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting
return taskHighlight->disable(); return taskHighlight->disable();
} }
static void inittaskHighlight() static void inittaskHighlight()
@@ -190,18 +190,6 @@ void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c)
} }
} }
// Used in Renderer for clearing the tile
int16_t InkHUD::Tile::getLeft()
{
return left;
}
// Used in Renderer for clearing the tile
int16_t InkHUD::Tile::getTop()
{
return top;
}
// Called by Applet base class, when setting applet dimensions, immediately before render // Called by Applet base class, when setting applet dimensions, immediately before render
uint16_t InkHUD::Tile::getWidth() uint16_t InkHUD::Tile::getWidth()
{ {
@@ -232,7 +220,7 @@ void InkHUD::Tile::requestHighlight()
{ {
Tile::highlightTarget = this; Tile::highlightTarget = this;
Tile::highlightShown = false; Tile::highlightShown = false;
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true); inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST);
} }
// Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first // Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first

View File

@@ -29,8 +29,6 @@ class Tile
void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout
void setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually void setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually
void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet
int16_t getLeft();
int16_t getTop();
uint16_t getWidth(); uint16_t getWidth();
uint16_t getHeight(); uint16_t getHeight();
static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter

View File

@@ -4,7 +4,6 @@
#include "./Applets/System/AlignStick/AlignStickApplet.h" #include "./Applets/System/AlignStick/AlignStickApplet.h"
#include "./Applets/System/BatteryIcon/BatteryIconApplet.h" #include "./Applets/System/BatteryIcon/BatteryIconApplet.h"
#include "./Applets/System/Keyboard/KeyboardApplet.h"
#include "./Applets/System/Logo/LogoApplet.h" #include "./Applets/System/Logo/LogoApplet.h"
#include "./Applets/System/Menu/MenuApplet.h" #include "./Applets/System/Menu/MenuApplet.h"
#include "./Applets/System/Notification/NotificationApplet.h" #include "./Applets/System/Notification/NotificationApplet.h"
@@ -149,28 +148,6 @@ void InkHUD::WindowManager::openAlignStick()
} }
} }
void InkHUD::WindowManager::openKeyboard()
{
KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard");
if (keyboard) {
keyboard->bringToForeground();
keyboardOpen = true;
changeLayout();
}
}
void InkHUD::WindowManager::closeKeyboard()
{
KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard");
if (keyboard) {
keyboard->sendToBackground();
keyboardOpen = false;
changeLayout();
}
}
// On the currently focussed tile: cycle to the next available user applet // On the currently focussed tile: cycle to the next available user applet
// Applets available for this must be activated, and not already displayed on another tile // Applets available for this must be activated, and not already displayed on another tile
void InkHUD::WindowManager::nextApplet() void InkHUD::WindowManager::nextApplet()
@@ -295,6 +272,7 @@ void InkHUD::WindowManager::toggleBatteryIcon()
batteryIcon->sendToBackground(); batteryIcon->sendToBackground();
// Force-render // Force-render
// - redraw all applets
inkhud->forceUpdate(EInk::UpdateTypes::FAST); inkhud->forceUpdate(EInk::UpdateTypes::FAST);
} }
@@ -333,25 +311,9 @@ void InkHUD::WindowManager::changeLayout()
menu->show(ft); menu->show(ft);
} }
// Resize for the on-screen keyboard
if (keyboardOpen) {
// Send all user applets to the background
// User applets currently don't handle free text input
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++)
inkhud->userApplets.at(i)->sendToBackground();
// Find the first system applet that can handle freetext and resize it
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleFreeText) {
const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight();
sa->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height() - keyboardHeight - 1);
break;
}
}
}
// Force-render // Force-render
// - redraw all applets // - redraw all applets
inkhud->forceUpdate(EInk::UpdateTypes::FAST, true); inkhud->forceUpdate(EInk::UpdateTypes::FAST);
} }
// Perform necessary reconfiguration when user activates or deactivates applets at run-time // Perform necessary reconfiguration when user activates or deactivates applets at run-time
@@ -385,7 +347,7 @@ void InkHUD::WindowManager::changeActivatedApplets()
// Force-render // Force-render
// - redraw all applets // - redraw all applets
inkhud->forceUpdate(EInk::UpdateTypes::FAST, true); inkhud->forceUpdate(EInk::UpdateTypes::FAST);
} }
// Some applets may be permitted to bring themselves to foreground, to show new data // Some applets may be permitted to bring themselves to foreground, to show new data
@@ -471,10 +433,8 @@ void InkHUD::WindowManager::createSystemApplets()
addSystemApplet("Logo", new LogoApplet, new Tile); addSystemApplet("Logo", new LogoApplet, new Tile);
addSystemApplet("Pairing", new PairingApplet, new Tile); addSystemApplet("Pairing", new PairingApplet, new Tile);
addSystemApplet("Tips", new TipsApplet, new Tile); addSystemApplet("Tips", new TipsApplet, new Tile);
if (settings->joystick.enabled) { if (settings->joystick.enabled)
addSystemApplet("AlignStick", new AlignStickApplet, new Tile); addSystemApplet("AlignStick", new AlignStickApplet, new Tile);
addSystemApplet("Keyboard", new KeyboardApplet, new Tile);
}
addSystemApplet("Menu", new MenuApplet, nullptr); addSystemApplet("Menu", new MenuApplet, nullptr);
@@ -497,13 +457,9 @@ void InkHUD::WindowManager::placeSystemTiles()
inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
if (settings->joystick.enabled) { if (settings->joystick.enabled)
inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight();
inkhud->getSystemApplet("Keyboard")
->getTile()
->setRegion(0, inkhud->height() - keyboardHeight, inkhud->width(), keyboardHeight);
}
inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20); inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20);
const uint16_t batteryIconHeight = Applet::getHeaderHeight() - 2 - 2; const uint16_t batteryIconHeight = Applet::getHeaderHeight() - 2 - 2;

View File

@@ -31,8 +31,6 @@ class WindowManager
void prevTile(); void prevTile();
void openMenu(); void openMenu();
void openAlignStick(); void openAlignStick();
void openKeyboard();
void closeKeyboard();
void nextApplet(); void nextApplet();
void prevApplet(); void prevApplet();
void rotate(); void rotate();
@@ -66,7 +64,6 @@ class WindowManager
void findOrphanApplets(); // Find any applets left-behind when layout changes void findOrphanApplets(); // Find any applets left-behind when layout changes
std::vector<Tile *> userTiles; // Tiles which can host user applets std::vector<Tile *> userTiles; // Tiles which can host user applets
bool keyboardOpen = false;
// For convenience // For convenience
InkHUD *inkhud = nullptr; InkHUD *inkhud = nullptr;

View File

@@ -174,7 +174,7 @@ class BasicExampleApplet : public Applet
// You must have an onRender() method // You must have an onRender() method
// All drawing happens here // All drawing happens here
void onRender(bool full) override; void onRender() override;
}; };
``` ```
@@ -183,7 +183,7 @@ The `onRender` method is called when the display image is redrawn. This can happ
```cpp ```cpp
// All drawing happens here // All drawing happens here
// Our basic example doesn't do anything useful. It just passively prints some text. // Our basic example doesn't do anything useful. It just passively prints some text.
void InkHUD::BasicExampleApplet::onRender(bool full) void InkHUD::BasicExampleApplet::onRender()
{ {
printAt(0, 0, "Hello, world!"); printAt(0, 0, "Hello, world!");
} }

View File

@@ -20,20 +20,20 @@ constexpr uint8_t modifierLeftShift = 0b0001;
// Num chars per key, Modulus for rotating through characters // Num chars per key, Modulus for rotating through characters
static uint8_t HackadayCommunicatorTapMod[_TCA8418_NUM_KEYS] = { static uint8_t HackadayCommunicatorTapMod[_TCA8418_NUM_KEYS] = {
0, 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, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2,
0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 2, 2, 2, 1, 2, 2, 0, 0, 0, 2, 1, 2, 2, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 2, 2, 2, 1, 2, 2, 0, 0, 0, 2, 1, 2, 2, 0, 1, 1, 0,
}; };
static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{}, static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{},
{Key::FUNCTION_F1}, {},
{'+'}, {'+'},
{'9'}, {'9'},
{'8'}, {'8'},
{'7'}, {'7'},
{Key::FUNCTION_F2}, {'2'},
{Key::FUNCTION_F3}, {'3'},
{Key::FUNCTION_F4}, {'4'},
{Key::FUNCTION_F5}, {'5'},
{Key::ESC}, {Key::ESC},
{'q', 'Q'}, {'q', 'Q'},
{'w', 'W'}, {'w', 'W'},
@@ -141,7 +141,6 @@ void HackadayCommunicatorKeyboard::pressed(uint8_t key)
if (state == Init || state == Busy) { if (state == Init || state == Busy) {
return; return;
} }
LOG_DEBUG("Key pressed: %u", key);
if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) {
modifierFlag = 0; modifierFlag = 0;

View File

@@ -1,59 +1,8 @@
#include "InputBroker.h" #include "InputBroker.h"
#include "PowerFSM.h" // needed for event trigger #include "PowerFSM.h" // needed for event trigger
#include "configuration.h" #include "configuration.h"
#include "graphics/Screen.h"
#include "modules/ExternalNotificationModule.h" #include "modules/ExternalNotificationModule.h"
#if ARCH_PORTDUINO
#include "input/LinuxInputImpl.h"
#include "input/SeesawRotary.h"
#include "platform/portduino/PortduinoGlue.h"
#endif
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
#include "input/ExpressLRSFiveWay.h"
#include "input/RotaryEncoderImpl.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/SerialKeyboardImpl.h"
#include "input/UpDownInterruptImpl1.h"
#include "input/i2cButton.h"
#if HAS_TRACKBALL
#include "input/TrackballInterruptImpl1.h"
#endif
#include "modules/StatusLEDModule.h"
#if !MESHTASTIC_EXCLUDE_I2C
#include "input/cardKbI2cImpl.h"
#endif
#include "input/kbMatrixImpl.h"
#endif
#if HAS_BUTTON || defined(ARCH_PORTDUINO)
#include "input/ButtonThread.h"
#if defined(BUTTON_PIN_TOUCH)
ButtonThread *TouchButtonThread = nullptr;
#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN)
static bool touchBacklightWasOn = false;
static bool touchBacklightActive = false;
#endif
#endif
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO)
ButtonThread *UserButtonThread = nullptr;
#endif
#if defined(ALT_BUTTON_PIN)
ButtonThread *BackButtonThread = nullptr;
#endif
#if defined(CANCEL_BUTTON_PIN)
ButtonThread *CancelButtonThread = nullptr;
#endif
#endif
InputBroker *inputBroker = nullptr; InputBroker *inputBroker = nullptr;
InputBroker::InputBroker() InputBroker::InputBroker()
@@ -125,262 +74,3 @@ void InputBroker::pollSoonWorker(void *p)
vTaskDelete(NULL); vTaskDelete(NULL);
} }
#endif #endif
void InputBroker::Init()
{
#ifdef BUTTON_PIN
#ifdef ARCH_ESP32
#if ESP_ARDUINO_VERSION_MAJOR >= 3
#ifdef BUTTON_NEED_PULLUP
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP);
#else
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
#endif
#else
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
#ifdef BUTTON_NEED_PULLUP
gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN));
delay(10);
#endif
#ifdef BUTTON_NEED_PULLUP2
gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2);
delay(10);
#endif
#endif
#endif
#endif
// buttons are now inputBroker, so have to come after setupModules
#if HAS_BUTTON
int pullup_sense = 0;
#ifdef INPUT_PULLUP_SENSE
// Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did
#ifdef BUTTON_SENSE_TYPE
pullup_sense = BUTTON_SENSE_TYPE;
#else
pullup_sense = INPUT_PULLUP_SENSE;
#endif
#endif
#if defined(ARCH_PORTDUINO)
if (portduino_config.userButtonPin.enabled) {
LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin);
UserButtonThread = new ButtonThread("UserButton");
if (screen) {
ButtonConfig config;
config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin;
config.activeLow = true;
config.activePullup = true;
config.pullupSense = INPUT_PULLUP;
config.intRoutine = []() {
UserButtonThread->userButton.tick();
UserButtonThread->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
concurrency::mainDelay.interruptFromISR(&higherWake);
};
config.singlePress = INPUT_BROKER_USER_PRESS;
config.longPress = INPUT_BROKER_SELECT;
UserButtonThread->initButton(config);
}
}
#endif
#ifdef BUTTON_PIN_TOUCH
TouchButtonThread = new ButtonThread("BackButton");
ButtonConfig touchConfig;
touchConfig.pinNumber = BUTTON_PIN_TOUCH;
touchConfig.activeLow = true;
touchConfig.activePullup = true;
touchConfig.pullupSense = pullup_sense;
touchConfig.intRoutine = []() {
TouchButtonThread->userButton.tick();
TouchButtonThread->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
concurrency::mainDelay.interruptFromISR(&higherWake);
};
touchConfig.singlePress = INPUT_BROKER_NONE;
touchConfig.longPress = INPUT_BROKER_BACK;
#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN)
// On T-Echo Plus the touch pad should only drive the backlight, not UI navigation/sounds
touchConfig.longPress = INPUT_BROKER_NONE;
touchConfig.suppressLeadUpSound = true;
touchConfig.onPress = []() {
touchBacklightWasOn = uiconfig.screen_brightness == 1;
if (!touchBacklightWasOn) {
digitalWrite(PIN_EINK_EN, HIGH);
}
touchBacklightActive = true;
};
touchConfig.onRelease = []() {
if (touchBacklightActive && !touchBacklightWasOn) {
digitalWrite(PIN_EINK_EN, LOW);
}
touchBacklightActive = false;
};
#endif
TouchButtonThread->initButton(touchConfig);
#endif
#if defined(CANCEL_BUTTON_PIN)
// Buttons. Moved here cause we need NodeDB to be initialized
CancelButtonThread = new ButtonThread("CancelButton");
ButtonConfig cancelConfig;
cancelConfig.pinNumber = CANCEL_BUTTON_PIN;
cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW;
cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP;
cancelConfig.pullupSense = pullup_sense;
cancelConfig.intRoutine = []() {
CancelButtonThread->userButton.tick();
CancelButtonThread->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
concurrency::mainDelay.interruptFromISR(&higherWake);
};
cancelConfig.singlePress = INPUT_BROKER_CANCEL;
cancelConfig.longPress = INPUT_BROKER_SHUTDOWN;
cancelConfig.longPressTime = 4000;
CancelButtonThread->initButton(cancelConfig);
#endif
#if defined(ALT_BUTTON_PIN)
// Buttons. Moved here cause we need NodeDB to be initialized
BackButtonThread = new ButtonThread("BackButton");
ButtonConfig backConfig;
backConfig.pinNumber = ALT_BUTTON_PIN;
backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW;
backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP;
backConfig.pullupSense = pullup_sense;
backConfig.intRoutine = []() {
BackButtonThread->userButton.tick();
BackButtonThread->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
concurrency::mainDelay.interruptFromISR(&higherWake);
};
backConfig.singlePress = INPUT_BROKER_ALT_PRESS;
backConfig.longPress = INPUT_BROKER_ALT_LONG;
backConfig.longPressTime = 500;
BackButtonThread->initButton(backConfig);
#endif
#if defined(BUTTON_PIN)
#if defined(USERPREFS_BUTTON_PIN)
int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN;
#else
int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN;
#endif
#ifndef BUTTON_ACTIVE_LOW
#define BUTTON_ACTIVE_LOW true
#endif
#ifndef BUTTON_ACTIVE_PULLUP
#define BUTTON_ACTIVE_PULLUP true
#endif
// Buttons. Moved here cause we need NodeDB to be initialized
// If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP
UserButtonThread = new ButtonThread("UserButton");
if (screen) {
ButtonConfig userConfig;
userConfig.pinNumber = (uint8_t)_pinNum;
userConfig.activeLow = BUTTON_ACTIVE_LOW;
userConfig.activePullup = BUTTON_ACTIVE_PULLUP;
userConfig.pullupSense = pullup_sense;
userConfig.intRoutine = []() {
UserButtonThread->userButton.tick();
UserButtonThread->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
concurrency::mainDelay.interruptFromISR(&higherWake);
};
userConfig.singlePress = INPUT_BROKER_USER_PRESS;
userConfig.longPress = INPUT_BROKER_SELECT;
userConfig.longPressTime = 500;
userConfig.longLongPress = INPUT_BROKER_SHUTDOWN;
UserButtonThread->initButton(userConfig);
} else {
ButtonConfig userConfigNoScreen;
userConfigNoScreen.pinNumber = (uint8_t)_pinNum;
userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW;
userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP;
userConfigNoScreen.pullupSense = pullup_sense;
userConfigNoScreen.intRoutine = []() {
UserButtonThread->userButton.tick();
UserButtonThread->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
concurrency::mainDelay.interruptFromISR(&higherWake);
};
userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS;
userConfigNoScreen.longPress = INPUT_BROKER_NONE;
userConfigNoScreen.longPressTime = 500;
userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN;
userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING;
userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE;
UserButtonThread->initButton(userConfigNoScreen);
}
#endif
#endif
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
#if defined(T_LORA_PAGER)
// use a special FSM based rotary encoder version for T-LoRa Pager
rotaryEncoderImpl = new RotaryEncoderImpl();
if (!rotaryEncoderImpl->init()) {
delete rotaryEncoderImpl;
rotaryEncoderImpl = nullptr;
}
#elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2)
upDownInterruptImpl1 = new UpDownInterruptImpl1();
if (!upDownInterruptImpl1->init()) {
delete upDownInterruptImpl1;
upDownInterruptImpl1 = nullptr;
}
#else
rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
if (!rotaryEncoderInterruptImpl1->init()) {
delete rotaryEncoderInterruptImpl1;
rotaryEncoderInterruptImpl1 = nullptr;
}
#endif
cardKbI2cImpl = new CardKbI2cImpl();
cardKbI2cImpl->init();
#if defined(M5STACK_UNITC6L)
i2cButton = new i2cButtonThread("i2cButtonThread");
#endif
#ifdef INPUTBROKER_MATRIX_TYPE
kbMatrixImpl = new KbMatrixImpl();
kbMatrixImpl->init();
#endif // INPUTBROKER_MATRIX_TYPE
#ifdef INPUTBROKER_SERIAL_TYPE
aSerialKeyboardImpl = new SerialKeyboardImpl();
aSerialKeyboardImpl->init();
#endif // INPUTBROKER_MATRIX_TYPE
}
#endif // HAS_BUTTON
#if ARCH_PORTDUINO
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") {
seesawRotary = new SeesawRotary("SeesawRotary");
if (!seesawRotary->init()) {
delete seesawRotary;
seesawRotary = nullptr;
}
aLinuxInputImpl = new LinuxInputImpl();
aLinuxInputImpl->init();
}
#endif
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
trackballInterruptImpl1 = new TrackballInterruptImpl1();
trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS);
}
#endif
#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE
expressLRSFiveWayInput = new ExpressLRSFiveWay();
#endif
}

View File

@@ -1,7 +1,6 @@
#pragma once #pragma once
#include "Observer.h" #include "Observer.h"
#include "concurrency/OSThread.h"
#include "freertosinc.h" #include "freertosinc.h"
#ifdef InputBrokerDebug #ifdef InputBrokerDebug
@@ -28,11 +27,6 @@ enum input_broker_event {
INPUT_BROKER_SHUTDOWN = 0x9b, INPUT_BROKER_SHUTDOWN = 0x9b,
INPUT_BROKER_GPS_TOGGLE = 0x9e, INPUT_BROKER_GPS_TOGGLE = 0x9e,
INPUT_BROKER_SEND_PING = 0xaf, INPUT_BROKER_SEND_PING = 0xaf,
INPUT_BROKER_FN_F1 = 0xf1,
INPUT_BROKER_FN_F2 = 0xf2,
INPUT_BROKER_FN_F3 = 0xf3,
INPUT_BROKER_FN_F4 = 0xf4,
INPUT_BROKER_FN_F5 = 0xf5,
INPUT_BROKER_MATRIXKEY = 0xFE, INPUT_BROKER_MATRIXKEY = 0xFE,
INPUT_BROKER_ANYKEY = 0xff INPUT_BROKER_ANYKEY = 0xff
@@ -77,7 +71,6 @@ class InputBroker : public Observable<const InputEvent *>
void queueInputEvent(const InputEvent *event); void queueInputEvent(const InputEvent *event);
void processInputEventQueue(); void processInputEventQueue();
#endif #endif
void Init();
protected: protected:
int handleInputEvent(const InputEvent *event); int handleInputEvent(const InputEvent *event);
@@ -91,5 +84,4 @@ class InputBroker : public Observable<const InputEvent *>
#endif #endif
}; };
extern InputBroker *inputBroker; extern InputBroker *inputBroker;
extern bool runASAP;

View File

@@ -5,6 +5,7 @@
SerialKeyboard *globalSerialKeyboard = nullptr; SerialKeyboard *globalSerialKeyboard = nullptr;
#ifdef INPUTBROKER_SERIAL_TYPE #ifdef INPUTBROKER_SERIAL_TYPE
#define CANNED_MESSAGE_MODULE_ENABLE 1 // in case it's not set in the variant file
#if INPUTBROKER_SERIAL_TYPE == 1 // It's a Chatter #if INPUTBROKER_SERIAL_TYPE == 1 // It's a Chatter
// 3 SHIFT level (lower case, upper case, numbers), up to 4 repeated presses, button number // 3 SHIFT level (lower case, upper case, numbers), up to 4 repeated presses, button number

View File

@@ -26,12 +26,7 @@ class TCA8418KeyboardBase
GPS_TOGGLE = 0x9E, GPS_TOGGLE = 0x9E,
MUTE_TOGGLE = 0xAC, MUTE_TOGGLE = 0xAC,
SEND_PING = 0xAF, SEND_PING = 0xAF,
BL_TOGGLE = 0xAB, BL_TOGGLE = 0xAB
FUNCTION_F1 = 0xF1,
FUNCTION_F2 = 0xF2,
FUNCTION_F3 = 0xF3,
FUNCTION_F4 = 0xF4,
FUNCTION_F5 = 0xF5
}; };
typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len);

View File

@@ -1,7 +1,5 @@
#include "TrackballInterruptBase.h" #include "TrackballInterruptBase.h"
#include "Throttle.h"
#include "configuration.h" #include "configuration.h"
extern bool osk_found; extern bool osk_found;
TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {} TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {}
@@ -57,27 +55,17 @@ int32_t TrackballInterruptBase::runOnce()
{ {
InputEvent e = {}; InputEvent e = {};
e.inputEvent = INPUT_BROKER_NONE; e.inputEvent = INPUT_BROKER_NONE;
#if TB_THRESHOLD
if (lastInterruptTime && !Throttle::isWithinTimespanMs(lastInterruptTime, 1000)) {
left_counter = 0;
right_counter = 0;
up_counter = 0;
down_counter = 0;
lastInterruptTime = 0;
}
#ifdef INPUT_DEBUG
if (left_counter > 0 || right_counter > 0 || up_counter > 0 || down_counter > 0) {
LOG_DEBUG("L %u R %u U %u D %u, time %u", left_counter, right_counter, up_counter, down_counter, millis());
}
#endif
#endif
// Handle long press detection for press button // Handle long press detection for press button
if (pressDetected && pressStartTime > 0) { if (pressDetected && pressStartTime > 0) {
uint32_t pressDuration = millis() - pressStartTime; uint32_t pressDuration = millis() - pressStartTime;
bool buttonStillPressed = false; bool buttonStillPressed = false;
#if defined(T_DECK)
buttonStillPressed = (this->action == TB_ACTION_PRESSED);
#else
buttonStillPressed = !digitalRead(_pinPress); buttonStillPressed = !digitalRead(_pinPress);
#endif
if (!buttonStillPressed) { if (!buttonStillPressed) {
// Button released // Button released
@@ -146,31 +134,23 @@ int32_t TrackballInterruptBase::runOnce()
} }
} }
#if TB_THRESHOLD #if defined(T_DECK) // T-deck gets a super-simple debounce on trackball
if (this->action == TB_ACTION_PRESSED && (!pressDetected || pressStartTime == 0)) { if (this->action == TB_ACTION_PRESSED && !pressDetected) {
// Start long press detection // Start long press detection
pressDetected = true; pressDetected = true;
pressStartTime = millis(); pressStartTime = millis();
// Don't send event yet, wait to see if it's a long press // Don't send event yet, wait to see if it's a long press
} else if (up_counter >= TB_THRESHOLD) { } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) {
#ifdef INPUT_DEBUG // LOG_DEBUG("Trackball event UP");
LOG_DEBUG("Trackball event UP %u", millis());
#endif
e.inputEvent = this->_eventUp; e.inputEvent = this->_eventUp;
} else if (down_counter >= TB_THRESHOLD) { } else if (this->action == TB_ACTION_DOWN && lastEvent == TB_ACTION_DOWN) {
#ifdef INPUT_DEBUG // LOG_DEBUG("Trackball event DOWN");
LOG_DEBUG("Trackball event DOWN %u", millis());
#endif
e.inputEvent = this->_eventDown; e.inputEvent = this->_eventDown;
} else if (left_counter >= TB_THRESHOLD) { } else if (this->action == TB_ACTION_LEFT && lastEvent == TB_ACTION_LEFT) {
#ifdef INPUT_DEBUG // LOG_DEBUG("Trackball event LEFT");
LOG_DEBUG("Trackball event LEFT %u", millis());
#endif
e.inputEvent = this->_eventLeft; e.inputEvent = this->_eventLeft;
} else if (right_counter >= TB_THRESHOLD) { } else if (this->action == TB_ACTION_RIGHT && lastEvent == TB_ACTION_RIGHT) {
#ifdef INPUT_DEBUG // LOG_DEBUG("Trackball event RIGHT");
LOG_DEBUG("Trackball event RIGHT %u", millis());
#endif
e.inputEvent = this->_eventRight; e.inputEvent = this->_eventRight;
} }
#else #else
@@ -203,12 +183,6 @@ int32_t TrackballInterruptBase::runOnce()
e.source = this->_originName; e.source = this->_originName;
e.kbchar = 0x00; e.kbchar = 0x00;
this->notifyObservers(&e); this->notifyObservers(&e);
#if TB_THRESHOLD
left_counter = 0;
right_counter = 0;
up_counter = 0;
down_counter = 0;
#endif
} }
// Only update lastEvent for non-press actions or completed press actions // Only update lastEvent for non-press actions or completed press actions
@@ -224,49 +198,25 @@ int32_t TrackballInterruptBase::runOnce()
void TrackballInterruptBase::intPressHandler() void TrackballInterruptBase::intPressHandler()
{ {
if (!Throttle::isWithinTimespanMs(lastInterruptTime, 10)) this->action = TB_ACTION_PRESSED;
this->action = TB_ACTION_PRESSED;
lastInterruptTime = millis();
} }
void TrackballInterruptBase::intDownHandler() void TrackballInterruptBase::intDownHandler()
{ {
if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) this->action = TB_ACTION_DOWN;
this->action = TB_ACTION_DOWN;
lastInterruptTime = millis();
#if TB_THRESHOLD
down_counter++;
#endif
} }
void TrackballInterruptBase::intUpHandler() void TrackballInterruptBase::intUpHandler()
{ {
if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) this->action = TB_ACTION_UP;
this->action = TB_ACTION_UP;
lastInterruptTime = millis();
#if TB_THRESHOLD
up_counter++;
#endif
} }
void TrackballInterruptBase::intLeftHandler() void TrackballInterruptBase::intLeftHandler()
{ {
if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) this->action = TB_ACTION_LEFT;
this->action = TB_ACTION_LEFT;
lastInterruptTime = millis();
#if TB_THRESHOLD
left_counter++;
#endif
} }
void TrackballInterruptBase::intRightHandler() void TrackballInterruptBase::intRightHandler()
{ {
if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) this->action = TB_ACTION_RIGHT;
this->action = TB_ACTION_RIGHT;
lastInterruptTime = millis();
#if TB_THRESHOLD
right_counter++;
#endif
} }

View File

@@ -12,10 +12,6 @@
#endif #endif
#endif #endif
#ifndef TB_THRESHOLD
#define TB_THRESHOLD 0
#endif
class TrackballInterruptBase : public Observable<const InputEvent *>, public concurrency::OSThread class TrackballInterruptBase : public Observable<const InputEvent *>, public concurrency::OSThread
{ {
public: public:
@@ -29,6 +25,8 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
void intUpHandler(); void intUpHandler();
void intLeftHandler(); void intLeftHandler();
void intRightHandler(); void intRightHandler();
uint32_t lastTime = 0;
virtual int32_t runOnce() override; virtual int32_t runOnce() override;
protected: protected:
@@ -69,12 +67,4 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
input_broker_event _eventPressedLong = INPUT_BROKER_NONE; input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
const char *_originName; const char *_originName;
TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE;
volatile uint32_t lastInterruptTime = 0;
#if TB_THRESHOLD
volatile uint8_t left_counter = 0;
volatile uint8_t right_counter = 0;
volatile uint8_t up_counter = 0;
volatile uint8_t down_counter = 0;
#endif
}; };

View File

@@ -24,26 +24,41 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe
void TrackballInterruptImpl1::handleIntDown() void TrackballInterruptImpl1::handleIntDown()
{ {
trackballInterruptImpl1->intDownHandler(); if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) {
trackballInterruptImpl1->setIntervalFromNow(20); trackballInterruptImpl1->lastTime = millis();
trackballInterruptImpl1->intDownHandler();
trackballInterruptImpl1->setIntervalFromNow(20);
}
} }
void TrackballInterruptImpl1::handleIntUp() void TrackballInterruptImpl1::handleIntUp()
{ {
trackballInterruptImpl1->intUpHandler(); if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) {
trackballInterruptImpl1->setIntervalFromNow(20); trackballInterruptImpl1->lastTime = millis();
trackballInterruptImpl1->intUpHandler();
trackballInterruptImpl1->setIntervalFromNow(20);
}
} }
void TrackballInterruptImpl1::handleIntLeft() void TrackballInterruptImpl1::handleIntLeft()
{ {
trackballInterruptImpl1->intLeftHandler(); if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) {
trackballInterruptImpl1->setIntervalFromNow(20); trackballInterruptImpl1->lastTime = millis();
trackballInterruptImpl1->intLeftHandler();
trackballInterruptImpl1->setIntervalFromNow(20);
}
} }
void TrackballInterruptImpl1::handleIntRight() void TrackballInterruptImpl1::handleIntRight()
{ {
trackballInterruptImpl1->intRightHandler(); if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) {
trackballInterruptImpl1->setIntervalFromNow(20); trackballInterruptImpl1->lastTime = millis();
trackballInterruptImpl1->intRightHandler();
trackballInterruptImpl1->setIntervalFromNow(20);
}
} }
void TrackballInterruptImpl1::handleIntPressed() void TrackballInterruptImpl1::handleIntPressed()
{ {
trackballInterruptImpl1->intPressHandler(); if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) {
trackballInterruptImpl1->setIntervalFromNow(20); trackballInterruptImpl1->lastTime = millis();
trackballInterruptImpl1->intPressHandler();
trackballInterruptImpl1->setIntervalFromNow(20);
}
} }

View File

@@ -321,26 +321,6 @@ int32_t KbI2cBase::runOnce()
e.inputEvent = INPUT_BROKER_ANYKEY; e.inputEvent = INPUT_BROKER_ANYKEY;
e.kbchar = INPUT_BROKER_MSG_TAB; e.kbchar = INPUT_BROKER_MSG_TAB;
break; break;
case TCA8418KeyboardBase::FUNCTION_F1:
e.inputEvent = INPUT_BROKER_FN_F1;
e.kbchar = 0x00;
break;
case TCA8418KeyboardBase::FUNCTION_F2:
e.inputEvent = INPUT_BROKER_FN_F2;
e.kbchar = 0x00;
break;
case TCA8418KeyboardBase::FUNCTION_F3:
e.inputEvent = INPUT_BROKER_FN_F3;
e.kbchar = 0x00;
break;
case TCA8418KeyboardBase::FUNCTION_F4:
e.inputEvent = INPUT_BROKER_FN_F4;
e.kbchar = 0x00;
break;
case TCA8418KeyboardBase::FUNCTION_F5:
e.inputEvent = INPUT_BROKER_FN_F5;
e.kbchar = 0x00;
break;
default: default:
if (nextEvent > 127) { if (nextEvent > 127) {
e.inputEvent = INPUT_BROKER_NONE; e.inputEvent = INPUT_BROKER_NONE;

View File

@@ -10,9 +10,9 @@
#include "ReliableRouter.h" #include "ReliableRouter.h"
#include "airtime.h" #include "airtime.h"
#include "buzz.h" #include "buzz.h"
#include "power/PowerHAL.h"
#include "FSCommon.h" #include "FSCommon.h"
#include "Led.h"
#include "RTC.h" #include "RTC.h"
#include "SPILock.h" #include "SPILock.h"
#include "Throttle.h" #include "Throttle.h"
@@ -42,6 +42,10 @@
#include "MessageStore.h" #include "MessageStore.h"
#endif #endif
#ifdef ELECROW_ThinkNode_M5
PCA9557 io(0x18, &Wire);
#endif
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
#include "freertosinc.h" #include "freertosinc.h"
#if !MESHTASTIC_EXCLUDE_WEBSERVER #if !MESHTASTIC_EXCLUDE_WEBSERVER
@@ -72,10 +76,29 @@ NRF52Bluetooth *nrf52Bluetooth = nullptr;
#include "mqtt/MQTT.h" #include "mqtt/MQTT.h"
#endif #endif
#include "LLCC68Interface.h"
#include "LR1110Interface.h"
#include "LR1120Interface.h"
#include "LR1121Interface.h"
#include "RF95Interface.h"
#include "SX1262Interface.h"
#include "SX1268Interface.h"
#include "SX1280Interface.h"
#include "detect/LoRaRadioType.h"
#ifdef ARCH_STM32WL
#include "STM32WLE5JCInterface.h"
#endif
#if defined(ARCH_PORTDUINO)
#include "platform/portduino/SimRadio.h"
#endif
#ifdef ARCH_PORTDUINO #ifdef ARCH_PORTDUINO
#include "linux/LinuxHardwareI2C.h" #include "linux/LinuxHardwareI2C.h"
#include "mesh/raspihttp/PiWebServer.h" #include "mesh/raspihttp/PiWebServer.h"
#include "platform/portduino/PortduinoGlue.h" #include "platform/portduino/PortduinoGlue.h"
#include "platform/portduino/USBHal.h"
#include <cstdlib> #include <cstdlib>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
@@ -119,6 +142,31 @@ void printPartitionTable()
#endif // DEBUG_PARTITION_TABLE #endif // DEBUG_PARTITION_TABLE
#endif // ARCH_ESP32 #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 "AmbientLightingThread.h"
#include "PowerFSMThread.h" #include "PowerFSMThread.h"
@@ -205,6 +253,9 @@ ScanI2C::DeviceAddress aqi_found = ScanI2C::ADDRESS_NONE;
Adafruit_DRV2605 drv; Adafruit_DRV2605 drv;
#endif #endif
// Global LoRa radio type
LoRaRadioType radioType = NO_RADIO;
bool isVibrating = false; bool isVibrating = false;
bool eink_found = true; bool eink_found = true;
@@ -241,8 +292,25 @@ const char *getDeviceName()
return name; return name;
} }
static int32_t ledBlinker()
{
// Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if
// config.device.led_heartbeat_disabled is changed
if (config.device.led_heartbeat_disabled)
return 1000;
static bool ledOn;
ledOn ^= 1;
ledBlink.set(ledOn);
// have a very sparse duty cycle of LED being on, unless charging, then blink 0.5Hz square wave rate to indicate that
return powerStatus->getIsCharging() ? 1000 : (ledOn ? 1 : 1000);
}
uint32_t timeLastPowered = 0; uint32_t timeLastPowered = 0;
static Periodic *ledPeriodic;
static OSThread *powerFSMthread; static OSThread *powerFSMthread;
static OSThread *ambientLightingThread; static OSThread *ambientLightingThread;
@@ -264,41 +332,6 @@ __attribute__((weak, noinline)) bool loopCanSleep()
void lateInitVariant() __attribute__((weak)); void lateInitVariant() __attribute__((weak));
void lateInitVariant() {} void lateInitVariant() {}
void earlyInitVariant() __attribute__((weak));
void earlyInitVariant() {}
// NRF52 (and probably other platforms) can report when system is in power failure mode
// (eg. too low battery voltage) and operating it is unsafe (data corruption, bootloops, etc).
// For example NRF52 will prevent any flash writes in that case automatically
// (but it causes issues we need to handle).
// This detection is independent from whatever ADC or dividers used in Meshtastic
// boards and is internal to chip.
// we use powerHAL layer to get this info and delay booting until power level is safe
// wait until power level is safe to continue booting (to avoid bootloops)
// blink user led in 3 flashes sequence to indicate what is happening
void waitUntilPowerLevelSafe()
{
while (powerHAL_isPowerLevelSafe() == false) {
#ifdef LED_POWER
// 3x: blink for 300 ms, pause for 300 ms
for (int i = 0; i < 3; i++) {
digitalWrite(LED_POWER, LED_STATE_ON);
delay(300);
digitalWrite(LED_POWER, LED_STATE_OFF);
delay(300);
}
#endif
// sleep for 2s
delay(2000);
}
}
/** /**
* Print info as a structured log message (for automated log processing) * Print info as a structured log message (for automated log processing)
*/ */
@@ -309,30 +342,34 @@ void printInfo()
#ifndef PIO_UNIT_TESTING #ifndef PIO_UNIT_TESTING
void setup() void setup()
{ {
#if defined(R1_NEO)
// initialize power HAL layer as early as possible pinMode(DCDC_EN_HOLD, OUTPUT);
powerHAL_init(); digitalWrite(DCDC_EN_HOLD, HIGH);
pinMode(NRF_ON, OUTPUT);
#ifdef LED_POWER digitalWrite(NRF_ON, HIGH);
pinMode(LED_POWER, OUTPUT);
digitalWrite(LED_POWER, LED_STATE_ON);
#endif #endif
// prevent booting if device is in power failure mode
// boot sequence will follow when battery level raises to safe mode
waitUntilPowerLevelSafe();
// Defined in variant.cpp for early init code
earlyInitVariant();
#if defined(PIN_POWER_EN) #if defined(PIN_POWER_EN)
pinMode(PIN_POWER_EN, OUTPUT); pinMode(PIN_POWER_EN, OUTPUT);
digitalWrite(PIN_POWER_EN, HIGH); digitalWrite(PIN_POWER_EN, HIGH);
#endif #endif
#ifdef LED_NOTIFICATION #if defined(ELECROW_ThinkNode_M5)
pinMode(LED_NOTIFICATION, OUTPUT); Wire.begin(48, 47);
digitalWrite(LED_NOTIFICATION, HIGH ^ LED_STATE_ON); io.pinMode(PCA_PIN_EINK_EN, OUTPUT);
io.pinMode(PCA_PIN_POWER_EN, OUTPUT);
io.digitalWrite(PCA_PIN_POWER_EN, HIGH);
// io.pinMode(C2_PIN, OUTPUT);
#endif
#ifdef LED_POWER
pinMode(LED_POWER, OUTPUT);
digitalWrite(LED_POWER, LED_STATE_ON);
#endif
#ifdef USER_LED
pinMode(USER_LED, OUTPUT);
digitalWrite(USER_LED, HIGH ^ LED_STATE_ON);
#endif #endif
#ifdef WIFI_LED #ifdef WIFI_LED
@@ -342,10 +379,75 @@ void setup()
#ifdef BLE_LED #ifdef BLE_LED
pinMode(BLE_LED, OUTPUT); pinMode(BLE_LED, OUTPUT);
digitalWrite(BLE_LED, LED_STATE_OFF); #ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, HIGH);
#else
digitalWrite(BLE_LED, LOW);
#endif
#endif
#if defined(T_DECK)
// GPIO10 manages all peripheral power supplies
// Turn on peripheral power immediately after MUC starts.
// If some boards are turned on late, ESP32 will reset due to low voltage.
// ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) ,
// TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder)
pinMode(KB_POWERON, OUTPUT);
digitalWrite(KB_POWERON, HIGH);
// T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus
// We need to initialize all CS pins in advance otherwise there will be SPI communication issues
// e.g. when detecting the SD card
pinMode(LORA_CS, OUTPUT);
digitalWrite(LORA_CS, HIGH);
pinMode(SDCARD_CS, OUTPUT);
digitalWrite(SDCARD_CS, HIGH);
pinMode(TFT_CS, OUTPUT);
digitalWrite(TFT_CS, HIGH);
delay(100);
#elif defined(T_DECK_PRO)
pinMode(LORA_EN, OUTPUT);
digitalWrite(LORA_EN, HIGH);
pinMode(LORA_CS, OUTPUT);
digitalWrite(LORA_CS, HIGH);
pinMode(SDCARD_CS, OUTPUT);
digitalWrite(SDCARD_CS, HIGH);
pinMode(PIN_EINK_CS, OUTPUT);
digitalWrite(PIN_EINK_CS, HIGH);
#elif defined(T_LORA_PAGER)
pinMode(LORA_CS, OUTPUT);
digitalWrite(LORA_CS, HIGH);
pinMode(SDCARD_CS, OUTPUT);
digitalWrite(SDCARD_CS, HIGH);
pinMode(TFT_CS, OUTPUT);
digitalWrite(TFT_CS, HIGH);
pinMode(KB_INT, INPUT_PULLUP);
// io expander
io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL);
io.pinMode(EXPANDS_DRV_EN, OUTPUT);
io.digitalWrite(EXPANDS_DRV_EN, HIGH);
io.pinMode(EXPANDS_AMP_EN, OUTPUT);
io.digitalWrite(EXPANDS_AMP_EN, LOW);
io.pinMode(EXPANDS_LORA_EN, OUTPUT);
io.digitalWrite(EXPANDS_LORA_EN, HIGH);
io.pinMode(EXPANDS_GPS_EN, OUTPUT);
io.digitalWrite(EXPANDS_GPS_EN, HIGH);
io.pinMode(EXPANDS_KB_EN, OUTPUT);
io.digitalWrite(EXPANDS_KB_EN, HIGH);
io.pinMode(EXPANDS_SD_EN, OUTPUT);
io.digitalWrite(EXPANDS_SD_EN, HIGH);
io.pinMode(EXPANDS_GPIO_EN, OUTPUT);
io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
io.pinMode(EXPANDS_SD_PULLEN, INPUT);
#elif defined(HACKADAY_COMMUNICATOR)
pinMode(KB_INT, INPUT);
#endif #endif
concurrency::hasBeenSetup = true; concurrency::hasBeenSetup = true;
#if ARCH_PORTDUINO
SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0);
#else
SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0);
#endif
meshtastic_Config_DisplayConfig_OledType screen_model = meshtastic_Config_DisplayConfig_OledType screen_model =
meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO;
@@ -456,10 +558,41 @@ void setup()
LOG_INFO("Wait for peripherals to stabilize"); LOG_INFO("Wait for peripherals to stabilize");
delay(PERIPHERAL_WARMUP_MS); delay(PERIPHERAL_WARMUP_MS);
#endif #endif
#ifdef BUTTON_PIN
#ifdef ARCH_ESP32
#if ESP_ARDUINO_VERSION_MAJOR >= 3
#ifdef BUTTON_NEED_PULLUP
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP);
#else
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
#endif
#else
pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
#ifdef BUTTON_NEED_PULLUP
gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN));
delay(10);
#endif
#ifdef BUTTON_NEED_PULLUP2
gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2);
delay(10);
#endif
#endif
#endif
#endif
initSPI(); initSPI();
OSThread::setup(); OSThread::setup();
#if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2)
// The ThinkNodes have their own blink logic
// ledPeriodic = new Periodic("Blink", elecrowLedBlinker);
#else
ledPeriodic = new Periodic("Blink", ledBlinker);
#endif
fsInit(); fsInit();
#if !MESHTASTIC_EXCLUDE_I2C #if !MESHTASTIC_EXCLUDE_I2C
@@ -684,6 +817,13 @@ void setup()
setupSDCard(); setupSDCard();
#endif #endif
// LED init
#ifdef LED_PIN
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LED_STATE_ON); // turn on for now
#endif
// Hello // Hello
printInfo(); printInfo();
#ifdef BUILD_EPOCH #ifdef BUILD_EPOCH
@@ -791,7 +931,7 @@ void setup()
SPI.begin(); SPI.begin();
#endif #endif
#else #else
// ESP32 // ESP32
#if defined(HW_SPI1_DEVICE) #if defined(HW_SPI1_DEVICE)
SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
@@ -823,7 +963,6 @@ void setup()
} }
#endif // HAS_SCREEN #endif // HAS_SCREEN
// TODO Remove magic string
// setup TZ prior to time actions. // setup TZ prior to time actions.
#if !MESHTASTIC_EXCLUDE_TZ #if !MESHTASTIC_EXCLUDE_TZ
LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string
@@ -907,9 +1046,180 @@ void setup()
nodeDB->hasWarned = true; nodeDB->hasWarned = true;
} }
#endif #endif
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
if (inputBroker) // buttons are now inputBroker, so have to come after setupModules
inputBroker->Init(); #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
#endif #endif
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
@@ -917,6 +1227,12 @@ void setup()
setupNicheGraphics(); setupNicheGraphics();
#endif #endif
#ifdef LED_PIN
// Turn LED off after boot, if heartbeat by config
if (config.device.led_heartbeat_disabled)
digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON);
#endif
// Do this after service.init (because that clears error_code) // Do this after service.init (because that clears error_code)
#ifdef HAS_PMU #ifdef HAS_PMU
if (!pmu_found) if (!pmu_found)
@@ -942,7 +1258,252 @@ void setup()
#endif #endif
#endif #endif
initLoRa(); #ifdef ARCH_PORTDUINO
// as one can't use a function pointer to the class constructor:
auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
RADIOLIB_PIN_TYPE busy) {
switch (portduino_config.lora_module) {
case use_rf95:
return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy);
case use_sx1262:
return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy);
case use_sx1268:
return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy);
case use_sx1280:
return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy);
case use_lr1110:
return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy);
case use_lr1120:
return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy);
case use_lr1121:
return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy);
case use_llcc68:
return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy);
case use_simradio:
return (RadioInterface *)new SimRadio;
default:
assert(0); // shouldn't happen
return (RadioInterface *)nullptr;
}
};
LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(),
portduino_config.lora_spi_dev.c_str());
if (portduino_config.lora_spi_dev == "ch341") {
RadioLibHAL = ch341Hal;
} else {
RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
}
rIf =
loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin,
portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin);
if (!rIf->init()) {
LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str());
delete rIf;
rIf = NULL;
exit(EXIT_FAILURE);
} else {
LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str());
}
#elif defined(HW_SPI1_DEVICE)
LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings);
#else // HW_SPI1_DEVICE
LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
#endif
// radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init)
#if defined(USE_STM32WLx)
if (!rIf) {
rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
if (!rIf->init()) {
LOG_WARN("No STM32WL radio");
delete rIf;
rIf = NULL;
} else {
LOG_INFO("STM32WL init success");
radioType = STM32WLx_RADIO;
}
}
#endif
#if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1);
if (!rIf->init()) {
LOG_WARN("No RF95 radio");
delete rIf;
rIf = NULL;
} else {
LOG_INFO("RF95 init success");
radioType = RF95_RADIO;
}
}
#endif
#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
#ifdef SX126X_DIO3_TCXO_VOLTAGE
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
#endif
if (!sxIf->init()) {
LOG_WARN("No SX1262 radio");
delete sxIf;
rIf = NULL;
} else {
LOG_INFO("SX1262 init success");
rIf = sxIf;
radioType = SX1262_RADIO;
}
}
#endif
#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL)
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
// try using the specified TCXO voltage
auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
if (!sxIf->init()) {
LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
delete sxIf;
rIf = NULL;
} else {
LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
rIf = sxIf;
radioType = SX1262_RADIO;
}
}
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
// If specified TCXO voltage fails, attempt to use DIO3 as a reference instead
rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
if (!rIf->init()) {
LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V");
delete rIf;
rIf = NULL;
} else {
LOG_INFO("SX1262 init success, XTAL, Vref 0.0V");
radioType = SX1262_RADIO;
}
}
#endif
#if defined(USE_SX1268)
#if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL)
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
// try using the specified TCXO voltage
auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
if (!sxIf->init()) {
LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
delete sxIf;
rIf = NULL;
} else {
LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
rIf = sxIf;
radioType = SX1268_RADIO;
}
}
#endif
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
if (!rIf->init()) {
LOG_WARN("No SX1268 radio");
delete rIf;
rIf = NULL;
} else {
LOG_INFO("SX1268 init success");
radioType = SX1268_RADIO;
}
}
#endif
#if defined(USE_LLCC68)
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
if (!rIf->init()) {
LOG_WARN("No LLCC68 radio");
delete rIf;
rIf = NULL;
} else {
LOG_INFO("LLCC68 init success");
radioType = LLCC68_RADIO;
}
}
#endif
#if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN);
if (!rIf->init()) {
LOG_WARN("No LR1110 radio");
delete rIf;
rIf = NULL;
} else {
LOG_INFO("LR1110 init success");
radioType = LR1110_RADIO;
}
}
#endif
#if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1
if (!rIf) {
rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN);
if (!rIf->init()) {
LOG_WARN("No LR1120 radio");
delete rIf;
rIf = NULL;
} else {
LOG_INFO("LR1120 init success");
radioType = LR1120_RADIO;
}
}
#endif
#if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1
if (!rIf) {
rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN);
if (!rIf->init()) {
LOG_WARN("No LR1121 radio");
delete rIf;
rIf = NULL;
} else {
LOG_INFO("LR1121 init success");
radioType = LR1121_RADIO;
}
}
#endif
#if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1
if (!rIf) {
rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY);
if (!rIf->init()) {
LOG_WARN("No SX1280 radio");
delete rIf;
rIf = NULL;
} else {
LOG_INFO("SX1280 init success");
radioType = SX1280_RADIO;
}
}
#endif
// check if the radio chip matches the selected region
if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && rIf && (!rIf->wideLora())) {
LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset");
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
nodeDB->saveToDisk(SEGMENT_CONFIG);
if (!rIf->reconfigure()) {
LOG_WARN("Reconfigure failed, rebooting");
if (screen) {
screen->showSimpleBanner("Rebooting...");
}
rebootAtMsec = millis() + 5000;
}
}
lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation) lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation)
@@ -1031,7 +1592,6 @@ bool suppressRebootBanner; // If true, suppress "Rebooting..." overlay (used for
// This will suppress the current delay and instead try to run ASAP. // This will suppress the current delay and instead try to run ASAP.
bool runASAP; bool runASAP;
// TODO find better home than main.cpp
extern meshtastic_DeviceMetadata getDeviceMetadata() extern meshtastic_DeviceMetadata getDeviceMetadata()
{ {
meshtastic_DeviceMetadata deviceMetadata; meshtastic_DeviceMetadata deviceMetadata;
@@ -1132,43 +1692,7 @@ void loop()
if (inputBroker) if (inputBroker)
inputBroker->processInputEventQueue(); inputBroker->processInputEventQueue();
#endif #endif
#if ARCH_PORTDUINO #if ARCH_PORTDUINO && HAS_TFT
if (portduino_config.lora_spi_dev == "ch341" && ch341Hal != nullptr) {
ch341Hal->checkError();
}
if (portduino_status.LoRa_in_error && rebootAtMsec == 0) {
LOG_ERROR("LoRa in error detected, attempting to recover");
if (rIf != nullptr) {
delete rIf;
rIf = nullptr;
}
if (portduino_config.lora_spi_dev == "ch341") {
if (ch341Hal != nullptr) {
delete ch341Hal;
ch341Hal = nullptr;
sleep(3);
}
try {
ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid,
portduino_config.lora_usb_pid);
} catch (std::exception &e) {
std::cerr << e.what() << std::endl;
std::cerr << "Could not initialize CH341 device!" << std::endl;
exit(EXIT_FAILURE);
}
}
if (initLoRa()) {
router->addInterface(rIf);
portduino_status.LoRa_in_error = false;
} else {
LOG_WARN("Reconfigure failed, rebooting");
if (screen) {
screen->showSimpleBanner("Rebooting...");
}
rebootAtMsec = millis() + 25;
}
}
#if HAS_TFT
if (screen && portduino_config.displayPanel == x11 && if (screen && portduino_config.displayPanel == x11 &&
config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
auto dispdev = screen->getDisplayDevice(); auto dispdev = screen->getDisplayDevice();
@@ -1176,7 +1700,6 @@ void loop()
static_cast<TFTDisplay *>(dispdev)->sdlLoop(); static_cast<TFTDisplay *>(dispdev)->sdlLoop();
} }
#endif #endif
#endif
#if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE #if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE
messageStoreAutosaveTick(); messageStoreAutosaveTick();
#endif #endif

View File

@@ -26,8 +26,8 @@ extern NRF52Bluetooth *nrf52Bluetooth;
#if ARCH_PORTDUINO #if ARCH_PORTDUINO
extern HardwareSPI *DisplaySPI; extern HardwareSPI *DisplaySPI;
extern HardwareSPI *LoraSPI; extern HardwareSPI *LoraSPI;
#endif
#endif
extern ScanI2C::DeviceAddress screen_found; extern ScanI2C::DeviceAddress screen_found;
extern ScanI2C::DeviceAddress cardkb_found; extern ScanI2C::DeviceAddress cardkb_found;
extern uint8_t kb_model; extern uint8_t kb_model;
@@ -47,16 +47,16 @@ extern bool isUSBPowered;
extern Adafruit_DRV2605 drv; extern Adafruit_DRV2605 drv;
#endif #endif
#ifdef HAS_PCA9557
#include <PCA9557.h>
extern PCA9557 io;
#endif
#ifdef HAS_I2S #ifdef HAS_I2S
#include "AudioThread.h" #include "AudioThread.h"
extern AudioThread *audioThread; extern AudioThread *audioThread;
#endif #endif
#ifdef ELECROW_ThinkNode_M5
#include <PCA9557.h>
extern PCA9557 io;
#endif
#ifdef HAS_UDP_MULTICAST #ifdef HAS_UDP_MULTICAST
#include "mesh/udp/UdpMulticastHandler.h" #include "mesh/udp/UdpMulticastHandler.h"
extern UdpMulticastHandler *udpHandler; extern UdpMulticastHandler *udpHandler;

View File

@@ -61,6 +61,11 @@ bool CryptoEngine::regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey)
return true; return true;
} }
#endif #endif
void CryptoEngine::clearKeys()
{
memset(public_key, 0, sizeof(public_key));
memset(private_key, 0, sizeof(private_key));
}
/** /**
* Encrypt a packet's payload using a key generated with Curve25519 and SHA256 * Encrypt a packet's payload using a key generated with Curve25519 and SHA256

View File

@@ -37,6 +37,7 @@ class CryptoEngine
virtual bool regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey); virtual bool regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey);
#endif #endif
void clearKeys();
void setDHPrivateKey(uint8_t *_private_key); void setDHPrivateKey(uint8_t *_private_key);
virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic,
uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut); uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut);

View File

@@ -91,21 +91,10 @@ template <typename T> bool LR11x0Interface<T>::init()
LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz");
#endif #endif
// Allow extra time for TCXO to stabilize after power-on
delay(10);
int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
// Retry if we get SPI command failed - some units need extra TCXO stabilization time
if (res == RADIOLIB_ERR_SPI_CMD_FAILED) {
LOG_WARN("LR11x0 init failed with %d (SPI_CMD_FAILED), retrying after delay...", res);
delay(100);
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
}
// \todo Display actual typename of the adapter, not just `LR11x0` // \todo Display actual typename of the adapter, not just `LR11x0`
LOG_INFO("LR11x0 init result %d", res); LOG_INFO("LR11x0 init result %d", res);
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) if (res == RADIOLIB_ERR_CHIP_NOT_FOUND)
return false; return false;
LR11x0VersionInfo_t version; LR11x0VersionInfo_t version;
@@ -170,7 +159,7 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
if (err != RADIOLIB_ERR_NONE) if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it err = lora.setCodingRate(cr);
if (err != RADIOLIB_ERR_NONE) if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);

View File

@@ -22,99 +22,4 @@ struct RegionInfo {
extern const RegionInfo regions[]; extern const RegionInfo regions[];
extern const RegionInfo *myRegion; extern const RegionInfo *myRegion;
extern void initRegion(); extern void initRegion();
static inline float bwCodeToKHz(uint16_t bwCode)
{
if (bwCode == 31)
return 31.25f;
if (bwCode == 62)
return 62.5f;
if (bwCode == 200)
return 203.125f;
if (bwCode == 400)
return 406.25f;
if (bwCode == 800)
return 812.5f;
if (bwCode == 1600)
return 1625.0f;
return (float)bwCode;
}
static inline uint16_t bwKHzToCode(float bwKHz)
{
if (bwKHz > 31.24f && bwKHz < 31.26f)
return 31;
if (bwKHz > 62.49f && bwKHz < 62.51f)
return 62;
if (bwKHz > 203.12f && bwKHz < 203.13f)
return 200;
if (bwKHz > 406.24f && bwKHz < 406.26f)
return 400;
if (bwKHz > 812.49f && bwKHz < 812.51f)
return 800;
if (bwKHz > 1624.99f && bwKHz < 1625.01f)
return 1600;
return (uint16_t)(bwKHz + 0.5f);
}
static inline void modemPresetToParams(meshtastic_Config_LoRaConfig_ModemPreset preset, bool wideLora, float &bwKHz, uint8_t &sf,
uint8_t &cr)
{
switch (preset) {
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
bwKHz = wideLora ? 1625.0f : 500.0f;
cr = 5;
sf = 7;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
bwKHz = wideLora ? 812.5f : 250.0f;
cr = 5;
sf = 7;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
bwKHz = wideLora ? 812.5f : 250.0f;
cr = 5;
sf = 8;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
bwKHz = wideLora ? 812.5f : 250.0f;
cr = 5;
sf = 9;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
bwKHz = wideLora ? 812.5f : 250.0f;
cr = 5;
sf = 10;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
bwKHz = wideLora ? 1625.0f : 500.0f;
cr = 8;
sf = 11;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
bwKHz = wideLora ? 406.25f : 125.0f;
cr = 8;
sf = 11;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
bwKHz = wideLora ? 406.25f : 125.0f;
cr = 8;
sf = 12;
break;
default: // LONG_FAST (or illegal)
bwKHz = wideLora ? 812.5f : 250.0f;
cr = 5;
sf = 11;
break;
}
}
static inline float modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset preset, bool wideLora)
{
float bwKHz = 0;
uint8_t sf = 0;
uint8_t cr = 0;
modemPresetToParams(preset, wideLora, bwKHz, sf, cr);
return bwKHz;
}

View File

@@ -13,7 +13,6 @@
#include "PacketHistory.h" #include "PacketHistory.h"
#include "PowerFSM.h" #include "PowerFSM.h"
#include "RTC.h" #include "RTC.h"
#include "RadioInterface.h"
#include "Router.h" #include "Router.h"
#include "SPILock.h" #include "SPILock.h"
#include "SafeFile.h" #include "SafeFile.h"
@@ -27,7 +26,6 @@
#include <algorithm> #include <algorithm>
#include <pb_decode.h> #include <pb_decode.h>
#include <pb_encode.h> #include <pb_encode.h>
#include <power/PowerHAL.h>
#include <vector> #include <vector>
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
@@ -824,10 +822,16 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.output_ms = 500; moduleConfig.external_notification.output_ms = 500;
moduleConfig.external_notification.nag_timeout = 2; moduleConfig.external_notification.nag_timeout = 2;
#endif #endif
#if defined(LED_NOTIFICATION) #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)
moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.enabled = true;
moduleConfig.external_notification.output = LED_NOTIFICATION; moduleConfig.external_notification.output = PIN_LED2;
moduleConfig.external_notification.active = LED_STATE_ON; #if defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3)
moduleConfig.external_notification.active = false;
#else
moduleConfig.external_notification.active = true;
#endif
moduleConfig.external_notification.alert_message = true; moduleConfig.external_notification.alert_message = true;
moduleConfig.external_notification.output_ms = 1000; moduleConfig.external_notification.output_ms = 1000;
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
@@ -851,6 +855,15 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.output_ms = 100; moduleConfig.external_notification.output_ms = 100;
moduleConfig.external_notification.active = true; moduleConfig.external_notification.active = true;
#endif #endif
#ifdef ELECROW_ThinkNode_M1
// Default to Elecrow USER_LED (blue)
moduleConfig.external_notification.enabled = true;
moduleConfig.external_notification.output = USER_LED;
moduleConfig.external_notification.active = true;
moduleConfig.external_notification.alert_message = true;
moduleConfig.external_notification.output_ms = 1000;
moduleConfig.external_notification.nag_timeout = 60;
#endif
#ifdef T_LORA_PAGER #ifdef T_LORA_PAGER
moduleConfig.canned_message.updown1_enabled = true; moduleConfig.canned_message.updown1_enabled = true;
moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A; moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A;
@@ -1284,13 +1297,6 @@ void NodeDB::loadFromDisk()
LOG_INFO("Loaded saved config version %d", config.version); LOG_INFO("Loaded saved config version %d", config.version);
} }
} }
// Coerce LoRa config fields derived from presets while bootstrapping.
// Some clients/UI components display bandwidth/spread_factor directly from config even in preset mode.
if (config.has_lora && config.lora.use_preset) {
RadioInterface::bootstrapLoRaConfigFromPreset(config.lora);
}
if (backupSecurity.private_key.size > 0) { if (backupSecurity.private_key.size > 0) {
LOG_DEBUG("Restoring backup of security config"); LOG_DEBUG("Restoring backup of security config");
config.security = backupSecurity; config.security = backupSecurity;
@@ -1404,15 +1410,6 @@ void NodeDB::loadFromDisk()
if (portduino_config.has_configDisplayMode) { if (portduino_config.has_configDisplayMode) {
config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode; config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode;
} }
if (portduino_config.has_statusMessage) {
moduleConfig.has_statusmessage = true;
strncpy(moduleConfig.statusmessage.node_status, portduino_config.statusMessage.c_str(),
sizeof(moduleConfig.statusmessage.node_status));
moduleConfig.statusmessage.node_status[sizeof(moduleConfig.statusmessage.node_status) - 1] = '\0';
}
if (portduino_config.enable_UDP) {
config.network.enabled_protocols = true;
}
#endif #endif
} }
@@ -1421,14 +1418,6 @@ void NodeDB::loadFromDisk()
bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct, bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct,
bool fullAtomic) bool fullAtomic)
{ {
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
// and all writes will fail anyway. Device should be sleeping at this point anyway.
if (!powerHAL_isPowerLevelSafe()) {
LOG_ERROR("Error: trying to saveProto() on unsafe device power level.");
return false;
}
bool okay = false; bool okay = false;
#ifdef FSCom #ifdef FSCom
auto f = SafeFile(filename, fullAtomic); auto f = SafeFile(filename, fullAtomic);
@@ -1455,14 +1444,6 @@ bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_
bool NodeDB::saveChannelsToDisk() bool NodeDB::saveChannelsToDisk()
{ {
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
// and all writes will fail anyway.
if (!powerHAL_isPowerLevelSafe()) {
LOG_ERROR("Error: trying to saveChannelsToDisk() on unsafe device power level.");
return false;
}
#ifdef FSCom #ifdef FSCom
spiLock->lock(); spiLock->lock();
FSCom.mkdir("/prefs"); FSCom.mkdir("/prefs");
@@ -1473,14 +1454,6 @@ bool NodeDB::saveChannelsToDisk()
bool NodeDB::saveDeviceStateToDisk() bool NodeDB::saveDeviceStateToDisk()
{ {
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
// and all writes will fail anyway. Device should be sleeping at this point anyway.
if (!powerHAL_isPowerLevelSafe()) {
LOG_ERROR("Error: trying to saveDeviceStateToDisk() on unsafe device power level.");
return false;
}
#ifdef FSCom #ifdef FSCom
spiLock->lock(); spiLock->lock();
FSCom.mkdir("/prefs"); FSCom.mkdir("/prefs");
@@ -1493,14 +1466,6 @@ bool NodeDB::saveDeviceStateToDisk()
bool NodeDB::saveNodeDatabaseToDisk() bool NodeDB::saveNodeDatabaseToDisk()
{ {
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
// and all writes will fail anyway. Device should be sleeping at this point anyway.
if (!powerHAL_isPowerLevelSafe()) {
LOG_ERROR("Error: trying to saveNodeDatabaseToDisk() on unsafe device power level.");
return false;
}
#ifdef FSCom #ifdef FSCom
spiLock->lock(); spiLock->lock();
FSCom.mkdir("/prefs"); FSCom.mkdir("/prefs");
@@ -1513,14 +1478,6 @@ bool NodeDB::saveNodeDatabaseToDisk()
bool NodeDB::saveToDiskNoRetry(int saveWhat) bool NodeDB::saveToDiskNoRetry(int saveWhat)
{ {
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
// and all writes will fail anyway. Device should be sleeping at this point anyway.
if (!powerHAL_isPowerLevelSafe()) {
LOG_ERROR("Error: trying to saveToDiskNoRetry() on unsafe device power level.");
return false;
}
bool success = true; bool success = true;
#ifdef FSCom #ifdef FSCom
spiLock->lock(); spiLock->lock();
@@ -1553,7 +1510,6 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
moduleConfig.has_ambient_lighting = true; moduleConfig.has_ambient_lighting = true;
moduleConfig.has_audio = true; moduleConfig.has_audio = true;
moduleConfig.has_paxcounter = true; moduleConfig.has_paxcounter = true;
moduleConfig.has_statusmessage = true;
success &= success &=
saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig); saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig);
@@ -1577,14 +1533,6 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
bool NodeDB::saveToDisk(int saveWhat) bool NodeDB::saveToDisk(int saveWhat)
{ {
LOG_DEBUG("Save to disk %d", saveWhat); LOG_DEBUG("Save to disk %d", saveWhat);
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
// and all writes will fail anyway. Device should be sleeping at this point anyway.
if (!powerHAL_isPowerLevelSafe()) {
LOG_ERROR("Error: trying to saveToDisk() on unsafe device power level.");
return false;
}
bool success = saveToDiskNoRetry(saveWhat); bool success = saveToDiskNoRetry(saveWhat);
if (!success) { if (!success) {
@@ -2223,8 +2171,8 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location,
} else if (location == meshtastic_AdminMessage_BackupLocation_SD) { } else if (location == meshtastic_AdminMessage_BackupLocation_SD) {
// TODO: After more mainline SD card support // TODO: After more mainline SD card support
} }
#endif
return success; return success;
#endif
} }
/// Record an error that should be reported via analytics /// Record an error that should be reported via analytics
@@ -2242,10 +2190,7 @@ void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, co
// Currently portuino is mostly used for simulation. Make sure the user notices something really bad happened // Currently portuino is mostly used for simulation. Make sure the user notices something really bad happened
#ifdef ARCH_PORTDUINO #ifdef ARCH_PORTDUINO
LOG_ERROR("A critical failure occurred"); LOG_ERROR("A critical failure occurred, portduino is exiting");
// TODO: Determine if other critical errors should also cause an immediate exit exit(2);
if (code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE ||
code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE)
exit(2);
#endif #endif
} }

View File

@@ -177,9 +177,6 @@ bool RF95Interface::init()
int res = lora->begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength); int res = lora->begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength);
LOG_INFO("RF95 init result %d", res); LOG_INFO("RF95 init result %d", res);
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
return false;
LOG_INFO("Frequency set to %f", getFreq()); LOG_INFO("Frequency set to %f", getFreq());
LOG_INFO("Bandwidth set to %f", bw); LOG_INFO("Bandwidth set to %f", bw);
LOG_INFO("Power output set to %d", power); LOG_INFO("Power output set to %d", power);

Some files were not shown because too many files have changed in this diff Show More