Compare commits
71
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0c06897ee | ||
|
|
309d51a3e8 | ||
|
|
93f87c57b9 | ||
|
|
94ef2ae451 | ||
|
|
abef0d85a2 | ||
|
|
6b3f975ba5 | ||
|
|
e028663658 | ||
|
|
bf68b9e597 | ||
|
|
a9b98f47e9 | ||
|
|
90a3ac5938 | ||
|
|
38f15db1d0 | ||
|
|
da821ec663 | ||
|
|
124bffad84 | ||
|
|
f98abe00f3 | ||
|
|
56a33a07f7 | ||
|
|
ce80433e43 | ||
|
|
3d98622b96 | ||
|
|
d3691258d3 | ||
|
|
360c54f1f9 | ||
|
|
8c4900a52f | ||
|
|
bfb833982e | ||
|
|
1410f170f9 | ||
|
|
14e998e6c3 | ||
|
|
57f678240d | ||
|
|
99df0a6fe9 | ||
|
|
ce7444c1a1 | ||
|
|
e3ae0a6ab5 | ||
|
|
733430ed45 | ||
|
|
cd2466692f | ||
|
|
4b424f0234 | ||
|
|
a212d2e84f | ||
|
|
5154e81d0b | ||
|
|
5c1b6b2a23 | ||
|
|
de345939af | ||
|
|
ef51c7ec11 | ||
|
|
3e873c51b7 | ||
|
|
f86cb7781e | ||
|
|
fe23dcfa3a | ||
|
|
2d6f2ba1a4 | ||
|
|
5d55353939 | ||
|
|
c3a46d4d85 | ||
|
|
0e0e17928b | ||
|
|
a522973fe7 | ||
|
|
cbfa8d7ffd | ||
|
|
3feb155a5f | ||
|
|
64e35908c9 | ||
|
|
8a1d6d9285 | ||
|
|
ee441dd7b2 | ||
|
|
178ae0a7f1 | ||
|
|
a08872299e | ||
|
|
cc9d433db0 | ||
|
|
60303968bb | ||
|
|
d3c7f05baa | ||
|
|
1971e5ab13 | ||
|
|
e2fda6598c | ||
|
|
982440d21d | ||
|
|
f9fea562aa | ||
|
|
c366296ab4 | ||
|
|
a076d97eb6 | ||
|
|
32dcd90abf | ||
|
|
5b7a5b2c22 | ||
|
|
5bce26d9b7 | ||
|
|
ef734b73c7 | ||
|
|
c7748a1602 | ||
|
|
91f930d5c0 | ||
|
|
f2c5cb0a05 | ||
|
|
e10e13226d | ||
|
|
5e69bc6c3f | ||
|
|
f3cb2bff78 | ||
|
|
894c5556cf | ||
|
|
2f92eb8499 |
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 -c \"import json,sys,subprocess,shutil,os; f=json.load(sys.stdin).get('tool_input',{}).get('file_path',''); t=shutil.which('trunk') or os.path.expanduser('~/.cache/trunk/launcher/trunk'); f and os.path.exists(t) and subprocess.run([t,'fmt','--force',f],stderr=subprocess.DEVNULL)\" 2>/dev/null || true",
|
||||
"statusMessage": "Formatting..."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
|
||||
# trunk-ignore(hadolint/DL3008): apt packages are not pinned.
|
||||
# trunk-ignore(terrascan/AC_DOCKER_0002): apt packages are not pinned.
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \
|
||||
cmake git zip libgpiod-dev libjsoncpp-dev libbluetooth-dev libi2c-dev \
|
||||
libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \
|
||||
libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev libsdl2-dev && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* && \
|
||||
|
||||
@@ -31,7 +31,7 @@ cmake --install "$WORK/ulfius/$SANITIZER" --prefix /usr
|
||||
cd "$SRC/firmware"
|
||||
|
||||
PLATFORMIO_EXTRA_SCRIPTS=$(echo -e "pre:.clusterfuzzlite/platformio-clusterfuzzlite-pre.py\npost:.clusterfuzzlite/platformio-clusterfuzzlite-post.py")
|
||||
STATIC_LIBS=$(pkg-config --libs --static libulfius openssl libgpiod yaml-cpp bluez --silence-errors)
|
||||
STATIC_LIBS=$(pkg-config --libs --static libulfius openssl libgpiod yaml-cpp jsoncpp bluez --silence-errors)
|
||||
export PLATFORMIO_EXTRA_SCRIPTS
|
||||
export STATIC_LIBS
|
||||
export PLATFORMIO_WORKSPACE_DIR="$WORK/pio/$SANITIZER"
|
||||
|
||||
@@ -16,6 +16,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
libssl-dev \
|
||||
libulfius-dev \
|
||||
libyaml-cpp-dev \
|
||||
libjsoncpp-dev \
|
||||
pipx \
|
||||
pkg-config \
|
||||
python3 \
|
||||
|
||||
@@ -13,7 +13,7 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get -y update --fix-missing
|
||||
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release
|
||||
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev libjsoncpp-dev lsb-release
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -11,4 +11,4 @@ runs:
|
||||
- name: Install libs needed for native build
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev
|
||||
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev libjsoncpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev
|
||||
|
||||
@@ -609,20 +609,35 @@ Most workflows can be triggered manually via `workflow_dispatch` for testing.
|
||||
|
||||
### Native unit tests (C++)
|
||||
|
||||
Unit tests in `test/` directory with 12 test suites:
|
||||
Unit tests in `test/` directory with 17 test suites:
|
||||
|
||||
- `test_crypto/` - Cryptography
|
||||
- `test_mqtt/` - MQTT integration
|
||||
- `test_radio/` - Radio interface
|
||||
- `test_mesh_module/` - Module framework
|
||||
- `test_meshpacket_serializer/` - Packet serialization
|
||||
- `test_transmit_history/` - Retransmission tracking
|
||||
- `test_admin_radio/` - LoRa region/config validation and AdminModule dispatch
|
||||
- `test_atak/` - ATAK integration
|
||||
- `test_crypto/` - Cryptography
|
||||
- `test_default/` - Default configuration
|
||||
- `test_http_content_handler/` - HTTP handling
|
||||
- `test_mac_from_string/` - MAC address parsing
|
||||
- `test_mesh_module/` - Module framework
|
||||
- `test_meshpacket_serializer/` - Packet serialization
|
||||
- `test_mqtt/` - MQTT integration
|
||||
- `test_packet_history/` - Packet history tracking
|
||||
- `test_position_precision/` - Position precision helpers
|
||||
- `test_radio/` - Radio interface
|
||||
- `test_serial/` - Serial communication
|
||||
- `test_traffic_management/` - Traffic management
|
||||
- `test_transmit_history/` - Retransmission tracking
|
||||
- `test_type_conversions/` - NodeDB v25 type conversion (bitfield round-trips, NodeInfoLite)
|
||||
- `test_utf8/` - UTF-8 utilities
|
||||
|
||||
Run with: `pio test -e native`
|
||||
Run command (preferred — avoids pipe-buffering and the Ubuntu externally-managed-environment error):
|
||||
|
||||
```bash
|
||||
~/.platformio/penv/bin/python -m platformio test -e native -f test_your_suite > /tmp/test_out.txt 2>&1
|
||||
grep -E 'error:|PASS|FAIL|succeeded|failed' /tmp/test_out.txt
|
||||
tail -15 /tmp/test_out.txt
|
||||
```
|
||||
|
||||
Do **not** use `pio test … | tail -N` — it discards build errors and shows stale cached results. Do **not** use `pio test … | grep` — line-buffering makes the terminal appear hung until the process exits. Redirect to a file first, then grep.
|
||||
|
||||
Simulation testing: `bin/test-simulator.sh`
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
name: Post Firmware Size Comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [CI]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
post-size-comment:
|
||||
if: >
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion != 'cancelled' &&
|
||||
github.repository == 'meshtastic/firmware'
|
||||
continue-on-error: true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download size report
|
||||
id: download
|
||||
uses: actions/download-artifact@v8
|
||||
continue-on-error: true
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: size-report
|
||||
path: ./
|
||||
|
||||
- name: Post or update PR comment
|
||||
if: steps.download.outcome == 'success'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const marker = '<!-- firmware-size-report -->';
|
||||
const body = fs.readFileSync('./size-report.md', 'utf8');
|
||||
const prNumber = parseInt(fs.readFileSync('./pr-number.txt', 'utf8').trim(), 10);
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
@@ -245,47 +245,126 @@ jobs:
|
||||
path: ./*.elf
|
||||
retention-days: 30
|
||||
|
||||
shame:
|
||||
firmware-size-report:
|
||||
if: github.repository == 'meshtastic/firmware'
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head)
|
||||
fetch-depth: 0
|
||||
- name: Download the current manifests
|
||||
|
||||
- name: Download current manifests
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ./manifests-new/
|
||||
path: ./manifests/
|
||||
pattern: manifest-*
|
||||
merge-multiple: true
|
||||
- name: Upload combined manifests for later commit and global stats crunching.
|
||||
|
||||
- name: Collect current firmware sizes
|
||||
run: python3 bin/collect_sizes.py ./manifests/ ./current-sizes.json
|
||||
|
||||
- name: Upload size report artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
id: upload-manifest
|
||||
with:
|
||||
name: manifests-${{ github.sha }}
|
||||
name: firmware-sizes-${{ github.sha }}
|
||||
overwrite: true
|
||||
path: manifests-new/*.mt.json
|
||||
- name: Find the merge base
|
||||
path: ./current-sizes.json
|
||||
retention-days: 90
|
||||
|
||||
- name: Download baseline sizes from develop
|
||||
if: github.event_name == 'pull_request'
|
||||
run: echo "MERGE_BASE=$(git merge-base "origin/$base" "$head")" >> $GITHUB_ENV
|
||||
continue-on-error: true
|
||||
id: baseline-develop
|
||||
env:
|
||||
base: ${{ github.base_ref }}
|
||||
head: ${{ github.sha }}
|
||||
# Currently broken (for-loop through EVERY artifact -- rate limiting)
|
||||
# - name: Download the old manifests
|
||||
# if: github.event_name == 'pull_request'
|
||||
# run: gh run download -R "$repo" --name "manifests-$merge_base" --dir manifest-old/
|
||||
# env:
|
||||
# GH_TOKEN: ${{ github.token }}
|
||||
# merge_base: ${{ env.MERGE_BASE }}
|
||||
# repo: ${{ github.repository }}
|
||||
# - name: Do scan and post comment
|
||||
# if: github.event_name == 'pull_request'
|
||||
# run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
RUN_ID=$(gh run list -R "${{ github.repository }}" \
|
||||
--workflow CI --branch develop --status success \
|
||||
--limit 1 --json databaseId --jq '.[0].databaseId // empty')
|
||||
if [ -n "$RUN_ID" ]; then
|
||||
ARTIFACT_NAME=$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}/artifacts" \
|
||||
--jq '.artifacts[] | select(.name | startswith("firmware-sizes-")) | .name' | head -1)
|
||||
if [ -n "$ARTIFACT_NAME" ]; then
|
||||
gh run download "$RUN_ID" -R "${{ github.repository }}" \
|
||||
--name "$ARTIFACT_NAME" --dir ./baseline-develop/
|
||||
cp "./baseline-develop/${ARTIFACT_NAME}/current-sizes.json" ./develop-sizes.json
|
||||
echo "found=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
else
|
||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Download baseline sizes from master
|
||||
if: github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
id: baseline-master
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
RUN_ID=$(gh run list -R "${{ github.repository }}" \
|
||||
--workflow CI --branch master --status success \
|
||||
--limit 1 --json databaseId --jq '.[0].databaseId // empty')
|
||||
if [ -n "$RUN_ID" ]; then
|
||||
ARTIFACT_NAME=$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}/artifacts" \
|
||||
--jq '.artifacts[] | select(.name | startswith("firmware-sizes-")) | .name' | head -1)
|
||||
if [ -n "$ARTIFACT_NAME" ]; then
|
||||
gh run download "$RUN_ID" -R "${{ github.repository }}" \
|
||||
--name "$ARTIFACT_NAME" --dir ./baseline-master/
|
||||
cp "./baseline-master/${ARTIFACT_NAME}/current-sizes.json" ./master-sizes.json
|
||||
echo "found=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
else
|
||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Generate size comparison report
|
||||
if: github.event_name == 'pull_request'
|
||||
id: report
|
||||
run: |
|
||||
ARGS="./current-sizes.json"
|
||||
if [ -f ./develop-sizes.json ]; then
|
||||
ARGS="$ARGS --baseline develop:./develop-sizes.json"
|
||||
fi
|
||||
if [ -f ./master-sizes.json ]; then
|
||||
ARGS="$ARGS --baseline master:./master-sizes.json"
|
||||
fi
|
||||
REPORT=$(python3 bin/size_report.py $ARGS)
|
||||
if [ -z "$REPORT" ]; then
|
||||
echo "has_report=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_report=true" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo '<!-- firmware-size-report -->'
|
||||
echo '# Firmware Size Report'
|
||||
echo ''
|
||||
echo "$REPORT"
|
||||
echo ''
|
||||
echo '---'
|
||||
echo "*Updated for ${{ github.sha }}*"
|
||||
} > ./size-report.md
|
||||
cat ./size-report.md >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Save PR number
|
||||
if: github.event_name == 'pull_request' && steps.report.outputs.has_report == 'true'
|
||||
run: echo "${{ github.event.pull_request.number }}" > ./pr-number.txt
|
||||
|
||||
- name: Upload size report
|
||||
if: github.event_name == 'pull_request' && steps.report.outputs.has_report == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: size-report
|
||||
path: |
|
||||
./size-report.md
|
||||
./pr-number.txt
|
||||
retention-days: 5
|
||||
|
||||
release-artifacts:
|
||||
permissions: # Needed for 'gh release upload'.
|
||||
@@ -333,6 +412,7 @@ jobs:
|
||||
prerelease: true
|
||||
name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha
|
||||
tag_name: v${{ needs.version.outputs.long }}
|
||||
target_commitish: ${{ github.sha }}
|
||||
body: ${{ steps.release_notes.outputs.notes }}
|
||||
|
||||
- name: Download source deb
|
||||
|
||||
@@ -16,13 +16,18 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Update submodule
|
||||
if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' }}
|
||||
if: ${{ github.ref_name == 'master' || github.ref_name == 'develop' }}
|
||||
working-directory: protobufs
|
||||
env:
|
||||
# Use the branch that triggered the workflow as the protobuf branch.
|
||||
GIT_BRANCH: ${{ github.ref_name }}
|
||||
run: |
|
||||
git submodule update --remote protobufs
|
||||
git fetch --prune origin $GIT_BRANCH
|
||||
git checkout origin/$GIT_BRANCH
|
||||
|
||||
- name: Download nanopb
|
||||
run: |
|
||||
wget https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.9.1-linux-x86.tar.gz
|
||||
wget https://github.com/nanopb/nanopb/releases/download/nanopb-0.4.9.1/nanopb-0.4.9.1-linux-x86.tar.gz
|
||||
tar xvzf nanopb-0.4.9.1-linux-x86.tar.gz
|
||||
mv nanopb-0.4.9.1-linux-x86 nanopb-0.4.9
|
||||
|
||||
@@ -33,7 +38,7 @@ jobs:
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
branch: create-pull-request/update-protobufs
|
||||
branch: create-pull-request/update-protobufs-${{ github.ref_name }}
|
||||
labels: submodules
|
||||
title: Update protobufs and classes
|
||||
commit-message: Update protobufs
|
||||
|
||||
@@ -10,18 +10,18 @@ This file (`AGENTS.md`) is a short pointer + quick reference for agents that don
|
||||
|
||||
## Quick command reference
|
||||
|
||||
| Action | Command |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| Build a firmware variant | `pio run -e <env>` (e.g. `pio run -e rak4631`, `pio run -e heltec-v3`) |
|
||||
| Build native macOS host binary | `pio run -e native-macos` (Homebrew prereqs + CH341 LoRa setup in `variants/native/portduino/platformio.ini`) |
|
||||
| Clean + rebuild | `pio run -e <env> -t clean && pio run -e <env>` |
|
||||
| Flash a device | `pio run -e <env> -t upload --upload-port <port>` (or use the `pio_flash` MCP tool) |
|
||||
| Run firmware unit tests (native) | `pio test -e native` |
|
||||
| Run MCP hardware tests | `./mcp-server/run-tests.sh` |
|
||||
| Live TUI test runner | `mcp-server/.venv/bin/meshtastic-mcp-test-tui` |
|
||||
| Format before commit | `trunk fmt` |
|
||||
| Regenerate protobuf bindings | `bin/regen-protos.sh` |
|
||||
| Generate CI matrix | `./bin/generate_ci_matrix.py all [--level pr]` |
|
||||
| Action | Command |
|
||||
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Build a firmware variant | `pio run -e <env>` (e.g. `pio run -e rak4631`, `pio run -e heltec-v3`) |
|
||||
| Build native macOS host binary | `pio run -e native-macos` (Homebrew prereqs + CH341 LoRa setup in `variants/native/portduino/platformio.ini`) |
|
||||
| Clean + rebuild | `pio run -e <env> -t clean && pio run -e <env>` |
|
||||
| Flash a device | `pio run -e <env> -t upload --upload-port <port>` (or use the `pio_flash` MCP tool) |
|
||||
| Run firmware unit tests (native) | `~/.platformio/penv/bin/python -m platformio test -e native > /tmp/test_out.txt 2>&1` then `grep -E 'error:\|PASS\|FAIL\|succeeded\|failed' /tmp/test_out.txt` (redirect first — piping causes line-buffering) |
|
||||
| Run MCP hardware tests | `./mcp-server/run-tests.sh` |
|
||||
| Live TUI test runner | `mcp-server/.venv/bin/meshtastic-mcp-test-tui` |
|
||||
| Format before commit | `trunk fmt` |
|
||||
| Regenerate protobuf bindings | `bin/regen-protos.sh` |
|
||||
| Generate CI matrix | `./bin/generate_ci_matrix.py all [--level pr]` |
|
||||
|
||||
## MCP server (device + test automation)
|
||||
|
||||
@@ -108,7 +108,7 @@ Sequence these; don't parallelize on the same port.
|
||||
| `src/modules/` | Feature modules; `Telemetry/Sensor/` has 50+ I2C sensor drivers |
|
||||
| `variants/` | 200+ hardware variant definitions (`variant.h` + `platformio.ini` per board) |
|
||||
| `protobufs/` | `.proto` definitions; regenerate with `bin/regen-protos.sh` |
|
||||
| `test/` | Firmware unit tests (12 suites; `pio test -e native`) |
|
||||
| `test/` | Firmware unit tests (17 suites; `pio test -e native`) |
|
||||
| `mcp-server/` | Python MCP server + pytest hardware integration tests |
|
||||
| `mcp-server/tests/` | Tiered pytest suite: `unit/`, `mesh/`, `telemetry/`, `monitor/`, `recovery/`, `ui/`, `fleet/`, `admin/`, `provisioning/` |
|
||||
| `.claude/commands/` | Claude Code slash command bodies |
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
curl wget g++ zip git ca-certificates pkg-config \
|
||||
python3-pip python3-grpc-tools \
|
||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||
libgpiod-dev libyaml-cpp-dev libjsoncpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
||||
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
@@ -53,7 +53,7 @@ ENV TZ=Etc/UTC
|
||||
USER root
|
||||
|
||||
RUN apt-get update && apt-get --no-install-recommends -y install \
|
||||
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
|
||||
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libjsoncpp26 libi2c0 libuv1t64 libusb-1.0-0-dev \
|
||||
liborcania2.3 libulfius2.7t64 libssl3t64 \
|
||||
libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
|
||||
# hadolint ignore=DL3008
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
g++ git ca-certificates pkg-config \
|
||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||
libgpiod-dev libyaml-cpp-dev libjsoncpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
||||
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
RUN apk --no-cache add \
|
||||
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
|
||||
py3-pip py3-grpcio-tools \
|
||||
libgpiod-dev yaml-cpp-dev bluez-dev \
|
||||
libgpiod-dev yaml-cpp-dev jsoncpp-dev bluez-dev \
|
||||
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
|
||||
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
@@ -48,7 +48,7 @@ LABEL org.opencontainers.image.title="Meshtastic" \
|
||||
USER root
|
||||
|
||||
RUN apk --no-cache add \
|
||||
shadow libstdc++ libbsd libgpiod yaml-cpp libusb \
|
||||
shadow libstdc++ libbsd libgpiod yaml-cpp jsoncpp libusb \
|
||||
i2c-tools libuv libx11 libinput libxkbcommon sdl2 \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& mkdir -p /var/lib/meshtasticd \
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Collect firmware binary sizes from manifest (.mt.json) files into a single report."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def collect_sizes(manifest_dir):
|
||||
"""Scan manifest_dir for .mt.json files and return {board: size_bytes} dict."""
|
||||
sizes = {}
|
||||
for fname in sorted(os.listdir(manifest_dir)):
|
||||
if not fname.endswith(".mt.json"):
|
||||
continue
|
||||
path = os.path.join(manifest_dir, fname)
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
board = data.get("platformioTarget", fname.replace(".mt.json", ""))
|
||||
# Find the main firmware .bin size (largest .bin, excluding OTA/littlefs/bleota)
|
||||
bin_size = None
|
||||
for entry in data.get("files", []):
|
||||
name = entry.get("name", "")
|
||||
if name.startswith("firmware-") and name.endswith(".bin"):
|
||||
bin_size = entry["bytes"]
|
||||
break
|
||||
# Fallback: any .bin that isn't ota/littlefs/bleota
|
||||
if bin_size is None:
|
||||
for entry in data.get("files", []):
|
||||
name = entry.get("name", "")
|
||||
if name.endswith(".bin") and not any(
|
||||
x in name for x in ["littlefs", "bleota", "ota"]
|
||||
):
|
||||
bin_size = entry["bytes"]
|
||||
break
|
||||
if bin_size is not None:
|
||||
sizes[board] = bin_size
|
||||
return sizes
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print(f"Usage: {sys.argv[0]} <manifest_dir> <output.json>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
manifest_dir = sys.argv[1]
|
||||
output_path = sys.argv[2]
|
||||
|
||||
sizes = collect_sizes(manifest_dir)
|
||||
with open(output_path, "w") as f:
|
||||
json.dump(sizes, f, indent=2, sort_keys=True)
|
||||
|
||||
print(f"Collected sizes for {len(sizes)} targets -> {output_path}")
|
||||
@@ -5,7 +5,9 @@ Meta:
|
||||
- raspberry-pi
|
||||
|
||||
Lora:
|
||||
### RAK13300 in Slot 2 pins
|
||||
|
||||
### RAK13300 in Slot 2
|
||||
Module: sx1262
|
||||
IRQ: 18 #IO6
|
||||
Reset: 24 # IO4
|
||||
Busy: 19 # IO5
|
||||
@@ -13,5 +15,7 @@ Lora:
|
||||
Enable_Pins:
|
||||
- 26
|
||||
- 23
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.1
|
||||
# CS: 7
|
||||
@@ -5,14 +5,18 @@ Meta:
|
||||
- raspberry-pi
|
||||
|
||||
Lora:
|
||||
### RAK13302 in Slot 2 pins
|
||||
|
||||
### RAK13302 in Slot 2
|
||||
Module: sx1262
|
||||
IRQ: 18 #IO6
|
||||
Reset: 24 # IO4
|
||||
Busy: 19 # IO5
|
||||
# Ant_sw: 23 # IO3
|
||||
# Ant_sw: 23 # IO3
|
||||
Enable_Pins:
|
||||
- 26
|
||||
- 23
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.1
|
||||
# CS: 7
|
||||
TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8]
|
||||
@@ -1,95 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from github import Github
|
||||
|
||||
def parseFile(path):
|
||||
with open(path, "r") as f:
|
||||
data = json.loads(f)
|
||||
for file in data["files"]:
|
||||
if file["name"].endswith(".bin"):
|
||||
return file["name"], file["bytes"]
|
||||
|
||||
if len(sys.argv) != 4:
|
||||
print(f"expected usage: {sys.argv[0]} <PR number> <path to old-manifests> <path to new-manifests>")
|
||||
sys.exit(1)
|
||||
|
||||
pr_number = int(sys.argv[1])
|
||||
|
||||
token = os.getenv("GITHUB_TOKEN")
|
||||
if not token:
|
||||
raise EnvironmentError("GITHUB_TOKEN not found in environment.")
|
||||
|
||||
repo_name = os.getenv("GITHUB_REPOSITORY") # "owner/repo"
|
||||
if not repo_name:
|
||||
raise EnvironmentError("GITHUB_REPOSITORY not found in environment.")
|
||||
|
||||
oldFiles = sys.argv[2]
|
||||
old = set(os.path.join(oldFiles, f) for f in os.listdir(oldFiles) if os.path.isfile(f))
|
||||
newFiles = sys.argv[3]
|
||||
new = set(os.path.join(newFiles, f) for f in os.listdir(newFiles) if os.path.isfile(f))
|
||||
|
||||
startMarkdown = "# Target Size Changes\n\n"
|
||||
markdown = ""
|
||||
|
||||
newlyIntroduced = new - old
|
||||
if len(newlyIntroduced) > 0:
|
||||
markdown += "## Newly Introduced Targets\n\n"
|
||||
# create a table
|
||||
markdown += "| File | Size |\n"
|
||||
markdown += "| ---- | ---- |\n"
|
||||
for f in newlyIntroduced:
|
||||
name, size = parseFile(f)
|
||||
markdown += f"| `{name}` | {size}b |\n"
|
||||
|
||||
# do not log removed targets
|
||||
# PRs only run a small subset of builds, so removed targets are not meaningful
|
||||
# since they are very likely to just be not ran in PR CI
|
||||
|
||||
both = old & new
|
||||
degradations = []
|
||||
improvements = []
|
||||
for f in both:
|
||||
oldName, oldSize = parseFile(f)
|
||||
_, newSize = parseFile(f)
|
||||
if oldSize != newSize:
|
||||
if newSize < oldSize:
|
||||
improvements.append((oldName, oldSize, newSize))
|
||||
else:
|
||||
degradations.append((oldName, oldSize, newSize))
|
||||
|
||||
if len(degradations) > 0:
|
||||
markdown += "\n## Degradation\n\n"
|
||||
# create a table
|
||||
markdown += "| File | Difference | Old Size | New Size |\n"
|
||||
markdown += "| ---- | ---------- | -------- | -------- |\n"
|
||||
for oldName, oldSize, newSize in degradations:
|
||||
markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n"
|
||||
|
||||
if len(improvements) > 0:
|
||||
markdown += "\n## Improvement\n\n"
|
||||
# create a table
|
||||
markdown += "| File | Difference | Old Size | New Size |\n"
|
||||
markdown += "| ---- | ---------- | -------- | -------- |\n"
|
||||
for oldName, oldSize, newSize in improvements:
|
||||
markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n"
|
||||
|
||||
if len(markdown) == 0:
|
||||
markdown = "No changes in target sizes detected."
|
||||
|
||||
g = Github(token)
|
||||
repo = g.get_repo(repo_name)
|
||||
pr = repo.get_pull(pr_number)
|
||||
|
||||
existing_comment = None
|
||||
for comment in pr.get_issue_comments():
|
||||
if comment.body.startswith(startMarkdown):
|
||||
existing_comment = comment
|
||||
break
|
||||
|
||||
final_markdown = startMarkdown + markdown
|
||||
|
||||
if existing_comment:
|
||||
existing_comment.edit(body=final_markdown)
|
||||
else:
|
||||
pr.create_issue_comment(body=final_markdown)
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Compare firmware size reports and generate a markdown summary.
|
||||
|
||||
Usage:
|
||||
size_report.py <new_sizes.json> [--baseline <label>:<old_sizes.json>]...
|
||||
|
||||
Examples:
|
||||
# Compare PR against develop and master baselines
|
||||
size_report.py pr.json --baseline develop:develop.json --baseline master:master.json
|
||||
|
||||
# Single baseline comparison
|
||||
size_report.py pr.json --baseline develop:develop.json
|
||||
|
||||
# No baselines — shows sizes with blank delta columns
|
||||
size_report.py pr.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def load_sizes(path):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def format_delta(n):
|
||||
"""Format byte delta with sign and human-friendly suffix."""
|
||||
sign = "+" if n > 0 else ""
|
||||
if abs(n) >= 1024:
|
||||
return f"{sign}{n:,} ({sign}{n / 1024:.1f} KB)"
|
||||
return f"{sign}{n:,}"
|
||||
|
||||
|
||||
def generate_markdown(new_sizes, baselines, top_n=5):
|
||||
"""Generate a single table with current size and delta columns per baseline.
|
||||
|
||||
baselines: list of (label, old_sizes_dict), may be empty
|
||||
"""
|
||||
labels = [label for label, _ in baselines]
|
||||
|
||||
# Build rows: (board, current_size, [(delta, abs_delta) per baseline])
|
||||
rows = []
|
||||
for board in sorted(new_sizes):
|
||||
current = new_sizes[board]
|
||||
deltas = []
|
||||
for _, old_sizes in baselines:
|
||||
old = old_sizes.get(board)
|
||||
if old is not None:
|
||||
d = current - old
|
||||
deltas.append((d, abs(d)))
|
||||
else:
|
||||
deltas.append((None, 0))
|
||||
# Sort key: max abs delta across baselines (biggest changes first)
|
||||
max_abs = max((ad for _, ad in deltas), default=0)
|
||||
rows.append((board, current, deltas, max_abs))
|
||||
|
||||
rows.sort(key=lambda r: r[3], reverse=True)
|
||||
|
||||
# Summary line
|
||||
sections = []
|
||||
summary_parts = [f"{len(new_sizes)} targets"]
|
||||
for i, (label, old_sizes) in enumerate(baselines):
|
||||
increases = sum(
|
||||
1 for _, _, deltas, _ in rows if deltas[i][0] is not None and deltas[i][0] > 0
|
||||
)
|
||||
decreases = sum(
|
||||
1 for _, _, deltas, _ in rows if deltas[i][0] is not None and deltas[i][0] < 0
|
||||
)
|
||||
net = sum(
|
||||
deltas[i][0] for _, _, deltas, _ in rows if deltas[i][0] is not None
|
||||
)
|
||||
parts = []
|
||||
if increases:
|
||||
parts.append(f"{increases} increased")
|
||||
if decreases:
|
||||
parts.append(f"{decreases} decreased")
|
||||
if parts:
|
||||
parts.append(f"net {format_delta(net)}")
|
||||
summary_parts.append(f"vs `{label}`: {', '.join(parts)}")
|
||||
else:
|
||||
summary_parts.append(f"vs `{label}`: no changes")
|
||||
|
||||
if not baselines:
|
||||
summary_parts.append("no baseline available yet")
|
||||
|
||||
sections.append(f"**{' | '.join(summary_parts)}**\n")
|
||||
|
||||
# Table header
|
||||
header = "| Target | Size |"
|
||||
separator = "|--------|-----:|"
|
||||
for label in labels:
|
||||
header += f" vs `{label}` |"
|
||||
separator += "----------:|"
|
||||
sections.append(header)
|
||||
sections.append(separator)
|
||||
|
||||
def format_row(board, current, deltas):
|
||||
row = f"| `{board}` | {current:,} |"
|
||||
for d, _ in deltas:
|
||||
if d is None:
|
||||
row += " |"
|
||||
elif d == 0:
|
||||
row += " 0 |"
|
||||
else:
|
||||
icon = "📈" if d > 0 else "📉"
|
||||
row += f" {icon} {format_delta(d)} |"
|
||||
return row
|
||||
|
||||
# Top N rows always visible
|
||||
top = rows[:top_n]
|
||||
for board, current, deltas, _ in top:
|
||||
sections.append(format_row(board, current, deltas))
|
||||
|
||||
# Remaining rows in expandable section
|
||||
rest = rows[top_n:]
|
||||
if rest:
|
||||
sections.append("")
|
||||
sections.append(
|
||||
f"<details><summary>Show {len(rest)} more target(s)</summary>\n"
|
||||
)
|
||||
sections.append(header)
|
||||
sections.append(separator)
|
||||
for board, current, deltas, _ in rest:
|
||||
sections.append(format_row(board, current, deltas))
|
||||
sections.append("\n</details>")
|
||||
|
||||
sections.append("")
|
||||
return "\n".join(sections)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Compare firmware size reports")
|
||||
parser.add_argument("new_sizes", help="Path to new sizes JSON")
|
||||
parser.add_argument(
|
||||
"--baseline",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="LABEL:PATH",
|
||||
help="Baseline to compare against (e.g. develop:develop.json)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--top",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Number of top changes to show before collapsing (default: 5)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
new_sizes = load_sizes(args.new_sizes)
|
||||
|
||||
# Silence output when no targets were built — repo maintainer choice
|
||||
if not new_sizes:
|
||||
return
|
||||
|
||||
baselines = []
|
||||
for b in args.baseline:
|
||||
if ":" not in b:
|
||||
print(f"Error: baseline must be LABEL:PATH, got '{b}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
label, path = b.split(":", 1)
|
||||
baselines.append((label, load_sizes(path)))
|
||||
|
||||
md = generate_markdown(new_sizes, baselines, top_n=args.top)
|
||||
print(md)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Tests for bin/collect_sizes.py and bin/size_report.py."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "bin")
|
||||
|
||||
|
||||
def make_manifest(target, firmware_bytes, extra_files=None):
|
||||
"""Create a minimal .mt.json manifest dict."""
|
||||
files = [{"name": f"firmware-{target}-2.6.0.bin", "bytes": firmware_bytes}]
|
||||
if extra_files:
|
||||
files.extend(extra_files)
|
||||
return {
|
||||
"platformioTarget": target,
|
||||
"version": "2.6.0.test",
|
||||
"files": files,
|
||||
}
|
||||
|
||||
|
||||
def write_manifests(tmpdir, manifests):
|
||||
"""Write manifest dicts as .mt.json files into tmpdir."""
|
||||
for target, data in manifests.items():
|
||||
path = os.path.join(tmpdir, f"firmware-{target}.mt.json")
|
||||
with open(path, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
|
||||
def run_script(script, args):
|
||||
"""Run a Python script and return (returncode, stdout, stderr)."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, os.path.join(SCRIPTS_DIR, script)] + args,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.returncode, result.stdout, result.stderr
|
||||
|
||||
|
||||
def test_collect_sizes_basic():
|
||||
"""collect_sizes picks up firmware-*.bin entries from manifests."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
outfile = os.path.join(tmpdir, "sizes.json")
|
||||
manifests = {
|
||||
"heltec-v3": make_manifest("heltec-v3", 1048576),
|
||||
"rak4631": make_manifest("rak4631", 524288),
|
||||
"tbeam": make_manifest("tbeam", 786432),
|
||||
}
|
||||
write_manifests(tmpdir, manifests)
|
||||
|
||||
rc, stdout, stderr = run_script("collect_sizes.py", [tmpdir, outfile])
|
||||
assert rc == 0, f"collect_sizes failed: {stderr}"
|
||||
assert "3 targets" in stdout
|
||||
|
||||
with open(outfile) as f:
|
||||
sizes = json.load(f)
|
||||
assert sizes == {"heltec-v3": 1048576, "rak4631": 524288, "tbeam": 786432}
|
||||
|
||||
|
||||
def test_collect_sizes_fallback_bin():
|
||||
"""collect_sizes falls back to non-firmware-prefixed .bin if no firmware-*.bin."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
outfile = os.path.join(tmpdir, "sizes.json")
|
||||
# Manifest with only a generic .bin (no firmware- prefix)
|
||||
data = {
|
||||
"platformioTarget": "custom-board",
|
||||
"files": [
|
||||
{"name": "littlefs-custom-board.bin", "bytes": 100000},
|
||||
{"name": "custom-board.bin", "bytes": 500000},
|
||||
],
|
||||
}
|
||||
path = os.path.join(tmpdir, "firmware-custom-board.mt.json")
|
||||
with open(path, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
rc, stdout, stderr = run_script("collect_sizes.py", [tmpdir, outfile])
|
||||
assert rc == 0, f"collect_sizes failed: {stderr}"
|
||||
|
||||
with open(outfile) as f:
|
||||
sizes = json.load(f)
|
||||
assert sizes == {"custom-board": 500000}
|
||||
|
||||
|
||||
def test_collect_sizes_skips_ota_littlefs():
|
||||
"""collect_sizes ignores ota/littlefs/bleota .bin files in fallback."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
outfile = os.path.join(tmpdir, "sizes.json")
|
||||
data = {
|
||||
"platformioTarget": "board-x",
|
||||
"files": [
|
||||
{"name": "littlefs-board-x.bin", "bytes": 100000},
|
||||
{"name": "bleota-board-x.bin", "bytes": 50000},
|
||||
{"name": "mt-board-x-ota.bin", "bytes": 60000},
|
||||
],
|
||||
}
|
||||
path = os.path.join(tmpdir, "firmware-board-x.mt.json")
|
||||
with open(path, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
rc, stdout, stderr = run_script("collect_sizes.py", [tmpdir, outfile])
|
||||
assert rc == 0
|
||||
with open(outfile) as f:
|
||||
sizes = json.load(f)
|
||||
# No valid firmware .bin found, board should be absent
|
||||
assert sizes == {}
|
||||
|
||||
|
||||
def test_collect_sizes_ignores_non_mt_json():
|
||||
"""collect_sizes skips non .mt.json files."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
outfile = os.path.join(tmpdir, "sizes.json")
|
||||
# Write a valid manifest
|
||||
manifests = {"rak4631": make_manifest("rak4631", 500000)}
|
||||
write_manifests(tmpdir, manifests)
|
||||
# Write a decoy file
|
||||
with open(os.path.join(tmpdir, "readme.txt"), "w") as f:
|
||||
f.write("not a manifest")
|
||||
|
||||
rc, stdout, stderr = run_script("collect_sizes.py", [tmpdir, outfile])
|
||||
assert rc == 0
|
||||
with open(outfile) as f:
|
||||
sizes = json.load(f)
|
||||
assert list(sizes.keys()) == ["rak4631"]
|
||||
|
||||
|
||||
def test_size_report_no_baseline():
|
||||
"""size_report with no baselines shows sizes only."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
sizes_file = os.path.join(tmpdir, "new.json")
|
||||
with open(sizes_file, "w") as f:
|
||||
json.dump({"heltec-v3": 1000000, "rak4631": 500000}, f)
|
||||
|
||||
rc, stdout, stderr = run_script("size_report.py", [sizes_file])
|
||||
assert rc == 0, f"size_report failed: {stderr}"
|
||||
assert "2 targets" in stdout
|
||||
assert "no baseline available yet" in stdout
|
||||
assert "`heltec-v3`" in stdout
|
||||
assert "`rak4631`" in stdout
|
||||
|
||||
|
||||
def test_size_report_with_baseline():
|
||||
"""size_report shows deltas against a baseline."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
new_file = os.path.join(tmpdir, "new.json")
|
||||
old_file = os.path.join(tmpdir, "old.json")
|
||||
with open(new_file, "w") as f:
|
||||
json.dump({"heltec-v3": 1050000, "rak4631": 500000, "tbeam": 800000}, f)
|
||||
with open(old_file, "w") as f:
|
||||
json.dump({"heltec-v3": 1000000, "rak4631": 500000, "tbeam": 810000}, f)
|
||||
|
||||
rc, stdout, stderr = run_script(
|
||||
"size_report.py", [new_file, "--baseline", f"develop:{old_file}"]
|
||||
)
|
||||
assert rc == 0, f"size_report failed: {stderr}"
|
||||
assert "3 targets" in stdout
|
||||
assert "1 increased" in stdout
|
||||
assert "1 decreased" in stdout
|
||||
# heltec-v3 grew by 50000
|
||||
assert "📈" in stdout
|
||||
# tbeam shrank by 10000
|
||||
assert "📉" in stdout
|
||||
# rak4631 unchanged
|
||||
assert "vs `develop`" in stdout
|
||||
|
||||
|
||||
def test_size_report_multiple_baselines():
|
||||
"""size_report handles multiple baselines."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
new_file = os.path.join(tmpdir, "new.json")
|
||||
dev_file = os.path.join(tmpdir, "develop.json")
|
||||
master_file = os.path.join(tmpdir, "master.json")
|
||||
with open(new_file, "w") as f:
|
||||
json.dump({"board-a": 100000}, f)
|
||||
with open(dev_file, "w") as f:
|
||||
json.dump({"board-a": 95000}, f)
|
||||
with open(master_file, "w") as f:
|
||||
json.dump({"board-a": 90000}, f)
|
||||
|
||||
rc, stdout, stderr = run_script(
|
||||
"size_report.py",
|
||||
[new_file, "--baseline", f"develop:{dev_file}", "--baseline", f"master:{master_file}"],
|
||||
)
|
||||
assert rc == 0, f"size_report failed: {stderr}"
|
||||
assert "vs `develop`" in stdout
|
||||
assert "vs `master`" in stdout
|
||||
|
||||
|
||||
def test_size_report_new_target_no_baseline_entry():
|
||||
"""size_report handles targets not present in baseline (new boards)."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
new_file = os.path.join(tmpdir, "new.json")
|
||||
old_file = os.path.join(tmpdir, "old.json")
|
||||
with open(new_file, "w") as f:
|
||||
json.dump({"new-board": 300000, "existing": 500000}, f)
|
||||
with open(old_file, "w") as f:
|
||||
json.dump({"existing": 500000}, f)
|
||||
|
||||
rc, stdout, stderr = run_script(
|
||||
"size_report.py", [new_file, "--baseline", f"develop:{old_file}"]
|
||||
)
|
||||
assert rc == 0, f"size_report failed: {stderr}"
|
||||
assert "`new-board`" in stdout
|
||||
assert "no changes" in stdout # only existing is compared, delta=0
|
||||
|
||||
|
||||
def test_size_report_all_unchanged():
|
||||
"""size_report shows 'no changes' when all sizes match."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
sizes_file = os.path.join(tmpdir, "sizes.json")
|
||||
with open(sizes_file, "w") as f:
|
||||
json.dump({"board-a": 100000, "board-b": 200000}, f)
|
||||
|
||||
rc, stdout, stderr = run_script(
|
||||
"size_report.py", [sizes_file, "--baseline", f"develop:{sizes_file}"]
|
||||
)
|
||||
assert rc == 0, f"size_report failed: {stderr}"
|
||||
assert "no changes" in stdout
|
||||
|
||||
|
||||
def test_collect_sizes_bad_args():
|
||||
"""collect_sizes exits with error on wrong arg count."""
|
||||
rc, stdout, stderr = run_script("collect_sizes.py", [])
|
||||
assert rc == 1
|
||||
assert "Usage" in stderr
|
||||
|
||||
|
||||
def test_size_report_bad_baseline_format():
|
||||
"""size_report exits with error on malformed --baseline."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
sizes_file = os.path.join(tmpdir, "sizes.json")
|
||||
with open(sizes_file, "w") as f:
|
||||
json.dump({"x": 1}, f)
|
||||
|
||||
rc, stdout, stderr = run_script(
|
||||
"size_report.py", [sizes_file, "--baseline", "no-colon-here"]
|
||||
)
|
||||
assert rc == 1
|
||||
assert "LABEL:PATH" in stderr
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
tests = [v for k, v in globals().items() if k.startswith("test_")]
|
||||
passed = 0
|
||||
failed = 0
|
||||
for test in tests:
|
||||
try:
|
||||
test()
|
||||
print(f" PASS: {test.__name__}")
|
||||
passed += 1
|
||||
except AssertionError as e:
|
||||
print(f" FAIL: {test.__name__}: {e}")
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f" ERROR: {test.__name__}: {type(e).__name__}: {e}")
|
||||
failed += 1
|
||||
|
||||
print(f"\n{passed} passed, {failed} failed out of {passed + failed}")
|
||||
sys.exit(1 if failed else 0)
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v6.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [
|
||||
["0x239A", "0x4405"],
|
||||
["0x239A", "0x0029"],
|
||||
["0x239A", "0x002A"],
|
||||
["0x2886", "0x1667"]
|
||||
],
|
||||
"usb_product": "HT-n5262",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "heltec_mesh_node_t1",
|
||||
"variants_dir": "variants",
|
||||
"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",
|
||||
"onboard_tools": ["jlink"],
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Heltec Mesh Node T1",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_size": 815104,
|
||||
"speed": 115200,
|
||||
"protocol": "nrfutil",
|
||||
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
|
||||
"use_1200bps_touch": true,
|
||||
"require_upload_port": true,
|
||||
"wait_for_upload_port": true
|
||||
},
|
||||
"url": "https://heltec.org",
|
||||
"vendor": "Heltec"
|
||||
}
|
||||
Vendored
+1
@@ -14,6 +14,7 @@ Build-Depends: debhelper-compat (= 13),
|
||||
g++,
|
||||
pkg-config,
|
||||
libyaml-cpp-dev,
|
||||
libjsoncpp-dev,
|
||||
libgpiod-dev,
|
||||
libbluetooth-dev,
|
||||
libusb-1.0-0-dev,
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
# trunk-ignore-all(ruff/F821)
|
||||
# trunk-ignore-all(flake8/F821)
|
||||
#
|
||||
# Whole-image LTO for nrf52840 (~-60KB; ~-23KB beyond src-only LTO), EXCEPT the objects
|
||||
# that own interrupt/exception handlers.
|
||||
#
|
||||
# Every ISR is referenced only from the assembly vector table (gcc_startup_nrf52840.S),
|
||||
# which LTO cannot see -> whole-program LTO judges the handlers dead, removes them, and
|
||||
# the weak `b .` Default_Handler stubs prevail -> the IRQ lands in an infinite loop and the
|
||||
# chip hangs (or the peripheral silently stalls). Compiling the handler-bearing objects
|
||||
# WITHOUT LTO lets ordinary linking keep the strong handlers; everything else stays LTO'd:
|
||||
# - framework core (/FrameworkArduino/, /cores/nRF5/): every nrfx ISR + the FreeRTOS
|
||||
# SVC/PendSV port.
|
||||
# - TinyUSB nrf port (Adafruit_TinyUSB_nrf.cpp): USBD_IRQHandler (USB data path).
|
||||
# - library .cpp files that own a vector ISR (would otherwise be silently dropped):
|
||||
# bluefruit.cpp -> SD_EVT/SWI2_EGU2 (SoftDevice BLE-event delivery -- advertising
|
||||
# hangs without it)
|
||||
# Wire_nRF52.cpp -> SPIM0/TWIM0 + SPIM1/TWIM1 (interrupt-driven I2C/SPI)
|
||||
# PDM.cpp -> PDM_IRQHandler (PDM microphone)
|
||||
# RotaryEncoder.cpp -> QDEC_IRQHandler (hardware quadrature/rotary encoder)
|
||||
#
|
||||
# A post-link guard (bottom of this file) fails the build if a critical handler was dropped
|
||||
# anyway -- so a future deps bump or a new ISR-owning library becomes a red build, not a field
|
||||
# hang. To hunt a dropped ISR by hand: nm the .elf for `_IRQHandler$` symbols marked `W`, then
|
||||
# grep the libs/framework for who defines them.
|
||||
#
|
||||
# HW-validated: RAK4631 (SX1262) + muzi-base (LR1121).
|
||||
import glob
|
||||
import os
|
||||
|
||||
Import("env")
|
||||
|
||||
env.Append(LINKFLAGS=["-flto", "-flto-partition=1to1"])
|
||||
|
||||
# The -fno-lto re-compiles below run with the global env, which lacks the framework's
|
||||
# bundled-library include dirs -- and those libs cross-include each other (Wire pulls in
|
||||
# Adafruit_TinyUSB.h, which pulls in SPI.h, ...). Add every bundled-lib dir (+ its src/) so
|
||||
# the re-compiles resolve without chasing headers one at a time.
|
||||
_fw = env.PioPlatform().get_package_dir("framework-arduinoadafruitnrf52") or ""
|
||||
_extra_inc = []
|
||||
for _d in sorted(glob.glob(os.path.join(_fw, "libraries", "*"))):
|
||||
if os.path.isdir(_d):
|
||||
_extra_inc.append(_d)
|
||||
if os.path.isdir(os.path.join(_d, "src")):
|
||||
_extra_inc.append(os.path.join(_d, "src"))
|
||||
|
||||
FRAMEWORK = ("/FrameworkArduino/", "/cores/nRF5/")
|
||||
USB_ISR = "Adafruit_TinyUSB_nrf" # USBD_IRQHandler
|
||||
# Library .cpp files that define vector-table ISRs (the rest of their lib stays LTO'd):
|
||||
LIB_ISR = ("/bluefruit.cpp", "/Wire_nRF52.cpp", "/PDM.cpp", "/RotaryEncoder.cpp")
|
||||
|
||||
|
||||
def _no_lto(node):
|
||||
try:
|
||||
path = node.get_abspath()
|
||||
except Exception:
|
||||
path = str(node)
|
||||
path = path.replace(
|
||||
"\\", "/"
|
||||
) # normalize Windows backslashes so matches work cross-platform
|
||||
if (
|
||||
USB_ISR in path
|
||||
or any(s in path for s in FRAMEWORK)
|
||||
or any(s in path for s in LIB_ISR)
|
||||
):
|
||||
return env.Object(
|
||||
node,
|
||||
CCFLAGS=env["CCFLAGS"] + ["-fno-lto"],
|
||||
CPPPATH=env["CPPPATH"] + _extra_inc,
|
||||
)
|
||||
return node
|
||||
|
||||
|
||||
env.AddBuildMiddleware(_no_lto)
|
||||
|
||||
|
||||
# --- post-link guard: catch a dropped ISR handler at build time (CI footgun protection) ----
|
||||
# After every link, fail the build if one of these critical vector-table handlers resolved to
|
||||
# the weak `b .` Default_Handler stub -- i.e. LTO (or a deps bump, or a new ISR-owning library
|
||||
# that nobody added to LIB_ISR) silently dropped it. A dropped handler hangs the chip the
|
||||
# instant that IRQ fires; this turns a field hang into a red build. CI builds every nrf52840
|
||||
# target, so this runs on every PR automatically. All five are used by every nrf52840
|
||||
# Meshtastic build; if a board deliberately stops using one, edit this tuple on purpose.
|
||||
_REQUIRED_STRONG = (
|
||||
"SWI2_EGU2_IRQHandler", # SoftDevice BLE event (SD_EVT) -- advertising & connections
|
||||
"GPIOTE_IRQHandler", # GPIO interrupts: radio DIO + buttons
|
||||
"RTC1_IRQHandler", # FreeRTOS scheduler tick
|
||||
"USBD_IRQHandler", # USB CDC (serial console + 1200bps DFU trigger)
|
||||
"POWER_CLOCK_IRQHandler", # HF/LF clock + power (HFCLK start for radio & SoftDevice)
|
||||
)
|
||||
|
||||
_tc = env.PioPlatform().get_package_dir("toolchain-gccarmnoneeabi") or ""
|
||||
_NM = os.path.join(_tc, "bin", "arm-none-eabi-nm")
|
||||
if not os.path.isfile(_NM):
|
||||
_NM = "arm-none-eabi-nm" # fall back to PATH
|
||||
|
||||
|
||||
def _assert_isr_handlers_survived(source, target, env):
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
try:
|
||||
# Resolve the ELF at build time; target[0] is the buildprog alias, not the file.
|
||||
elf = env.subst("$BUILD_DIR/${PROGNAME}.elf")
|
||||
out = subprocess.check_output([_NM, elf], universal_newlines=True)
|
||||
except Exception as exc: # tooling hiccup: warn loudly, don't wedge the build
|
||||
print("nrf52_lto: WARNING - ISR-handler guard skipped (nm failed: %s)" % exc)
|
||||
return
|
||||
# nm line: "<addr> <type> <symbol>". type 'T'/'t' = strong (good); 'W'/'w' = weak stub.
|
||||
kind = {}
|
||||
for line in out.split("\n"):
|
||||
f = line.split()
|
||||
if len(f) >= 3 and f[-1].endswith("_IRQHandler"):
|
||||
kind[f[-1]] = f[-2]
|
||||
dropped = [h for h in _REQUIRED_STRONG if kind.get(h, "W").upper() != "T"]
|
||||
if dropped:
|
||||
sys.stderr.write(
|
||||
"\n*** nrf52 LTO guard: interrupt handler(s) DROPPED: %s ***\n"
|
||||
"Each resolved to the weak Default_Handler stub, so the chip hangs when that IRQ\n"
|
||||
"fires. Compile the .cpp that defines the handler with -fno-lto by adding it to\n"
|
||||
"LIB_ISR in extra_scripts/nrf52_lto.py. Find the owner of FOO_IRQHandler with:\n"
|
||||
" grep -rl FOO_IRQHandler <framework-arduinoadafruitnrf52>/{libraries,cores}\n\n"
|
||||
% ", ".join(dropped)
|
||||
)
|
||||
from SCons.Script import Exit
|
||||
|
||||
Exit(1) # canonical SCons build-abort -> red build
|
||||
print(
|
||||
"nrf52_lto: ISR-handler guard OK -- %d critical handlers strong"
|
||||
% len(_REQUIRED_STRONG)
|
||||
)
|
||||
|
||||
|
||||
# Attach to the phony "buildprog" alias, NOT the .elf file node: SCons can skip a post-action
|
||||
# on a file target during an incremental relink (observed), but the buildprog alias runs every
|
||||
# build -- so the guard fires on local incremental rebuilds and clean CI builds alike.
|
||||
env.AddPostAction("buildprog", _assert_isr_handlers_survived)
|
||||
@@ -191,10 +191,12 @@ echo
|
||||
# PASS/FAIL — every hardware test would SKIP with "role not present". We
|
||||
# narrow to tests/unit explicitly so the summary reads as "no hardware,
|
||||
# unit suite only" instead of "big skip count looks suspicious".
|
||||
# Keep terminal output condensed (`-q -r fE`) so skip-heavy runs do not print
|
||||
# each skipped test in full; skip counts still appear in pytest's summary.
|
||||
if [[ -z $DETECTED && $# -eq 0 ]]; then
|
||||
echo "[pre-flight] no supported devices detected; running unit tier only."
|
||||
echo
|
||||
exec "$VENV_PY" -m pytest tests/unit -v --report-log=tests/reportlog.jsonl
|
||||
exec "$VENV_PY" -m pytest tests/unit -q -r fE --report-log=tests/reportlog.jsonl
|
||||
fi
|
||||
|
||||
# Default pytest args when the user passed none. Power users can invoke
|
||||
@@ -210,11 +212,13 @@ fi
|
||||
# skipping half the hardware tests with "not baked with session profile"
|
||||
# errors. Power users who know their hardware is current and want to shave
|
||||
# those seconds can pass `--assume-baked` explicitly.
|
||||
# Defaults also use condensed reporting (`-q -r fE`) to avoid listing every
|
||||
# skipped test verbatim while still surfacing failures/errors and summary data.
|
||||
if [[ $# -eq 0 ]]; then
|
||||
set -- tests/ \
|
||||
--html=tests/report.html --self-contained-html \
|
||||
--junitxml=tests/junit.xml \
|
||||
-v --tb=short
|
||||
-q -r fE --tb=short
|
||||
fi
|
||||
|
||||
# UI tier requires opencv-python-headless (and ideally easyocr). If it's
|
||||
|
||||
@@ -34,6 +34,7 @@ BuildRequires: python3dist(grpcio-tools)
|
||||
BuildRequires: git-core
|
||||
BuildRequires: gcc-c++
|
||||
BuildRequires: pkgconfig(yaml-cpp)
|
||||
BuildRequires: pkgconfig(jsoncpp)
|
||||
BuildRequires: pkgconfig(libgpiod)
|
||||
BuildRequires: pkgconfig(bluez)
|
||||
BuildRequires: pkgconfig(libusb-1.0)
|
||||
|
||||
+4
-6
@@ -186,12 +186,16 @@ lib_deps =
|
||||
https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library/archive/refs/tags/v1.1.2.zip
|
||||
# renovate: datasource=github-tags depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/SparkFun_ICM-20948_ArduinoLibrary
|
||||
https://github.com/sparkfun/SparkFun_ICM-20948_ArduinoLibrary/archive/refs/tags/v1.3.2.zip
|
||||
# renovate: datasource=github-tags depName=TDK InvenSense ICM42670P packageName=tdk-invn-oss/motion.arduino.ICM42670P
|
||||
https://github.com/tdk-invn-oss/motion.arduino.ICM42670P/archive/refs/tags/1.0.8.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit LTR390 Library packageName=adafruit/Adafruit_LTR390
|
||||
https://github.com/adafruit/Adafruit_LTR390/archive/refs/tags/1.1.2.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit PCT2075 packageName=adafruit/Adafruit_PCT2075
|
||||
https://github.com/adafruit/Adafruit_PCT2075/archive/refs/tags/1.0.6.zip
|
||||
# renovate: datasource=github-tags depName=DFRobot_BMM150 packageName=dfrobot/DFRobot_BMM150
|
||||
https://github.com/DFRobot/DFRobot_BMM150/archive/refs/tags/V1.0.0.zip
|
||||
# renovate: datasource=github-tags depName=SparkFun MMC5983MA Magnetometer packageName=sparkfun/SparkFun_MMC5983MA_Magnetometer_Arduino_Library
|
||||
https://github.com/sparkfun/SparkFun_MMC5983MA_Magnetometer_Arduino_Library/archive/refs/tags/v1.1.4.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit_TSL2561 packageName=adafruit/Adafruit_TSL2561
|
||||
https://github.com/adafruit/Adafruit_TSL2561/archive/refs/tags/1.1.3.zip
|
||||
# renovate: datasource=github-tags depName=BH1750_WE packageName=wollewald/BH1750_WE
|
||||
@@ -204,16 +208,10 @@ lib_deps =
|
||||
https://github.com/adafruit/Adafruit_BMP3XX/archive/refs/tags/2.1.6.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit MAX1704X packageName=adafruit/Adafruit_MAX1704X
|
||||
https://github.com/adafruit/Adafruit_MAX1704X/archive/refs/tags/1.0.3.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit SHTC3 packageName=adafruit/Adafruit_SHTC3
|
||||
https://github.com/adafruit/Adafruit_SHTC3/archive/refs/tags/1.0.2.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit LPS2X packageName=adafruit/Adafruit_LPS2X
|
||||
https://github.com/adafruit/Adafruit_LPS2X/archive/refs/tags/2.0.6.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit SHT31 packageName=adafruit/Adafruit_SHT31
|
||||
https://github.com/adafruit/Adafruit_SHT31/archive/refs/tags/2.2.2.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit VEML7700 packageName=adafruit/Adafruit_VEML7700
|
||||
https://github.com/adafruit/Adafruit_VEML7700/archive/refs/tags/2.1.6.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit SHT4x packageName=adafruit/Adafruit_SHT4X
|
||||
https://github.com/adafruit/Adafruit_SHT4X/archive/refs/tags/1.0.5.zip
|
||||
# renovate: datasource=github-tags depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/SparkFun_Qwiic_Scale_NAU7802_Arduino_Library
|
||||
https://github.com/sparkfun/SparkFun_Qwiic_Scale_NAU7802_Arduino_Library/archive/refs/tags/v1.0.6.zip
|
||||
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
|
||||
|
||||
+1
-1
Submodule protobufs updated: e978a1850b...7916a0ce81
@@ -152,7 +152,9 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...);
|
||||
// Default Bluetooth PIN
|
||||
#define defaultBLEPin 123456
|
||||
|
||||
#if HAS_ETHERNET && defined(USE_CH390D)
|
||||
#if HAS_ETHERNET && defined(USE_ARDUINO_ETHERNET)
|
||||
#include <Ethernet.h> // arduino-libraries/Ethernet — supports W5500 auto-detect
|
||||
#elif HAS_ETHERNET && defined(USE_CH390D)
|
||||
#include <ESP32_CH390.h>
|
||||
#elif HAS_ETHERNET && !defined(USE_WS5500)
|
||||
#include <RAK13800_W5100S.h>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "DisplayFormatters.h"
|
||||
#include "MeshRadio.h"
|
||||
|
||||
const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName,
|
||||
bool usePreset)
|
||||
@@ -11,33 +12,51 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
|
||||
}
|
||||
|
||||
switch (preset) {
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
|
||||
case PRESET(SHORT_TURBO):
|
||||
return useShortName ? "ShortT" : "ShortTurbo";
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
|
||||
case PRESET(SHORT_SLOW):
|
||||
return useShortName ? "ShortS" : "ShortSlow";
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
|
||||
case PRESET(SHORT_FAST):
|
||||
return useShortName ? "ShortF" : "ShortFast";
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
|
||||
case PRESET(MEDIUM_SLOW):
|
||||
return useShortName ? "MedS" : "MediumSlow";
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
|
||||
case PRESET(MEDIUM_FAST):
|
||||
return useShortName ? "MedF" : "MediumFast";
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
|
||||
case PRESET(LONG_SLOW):
|
||||
return useShortName ? "LongS" : "LongSlow";
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST:
|
||||
case PRESET(LONG_FAST):
|
||||
return useShortName ? "LongF" : "LongFast";
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
|
||||
case PRESET(LONG_TURBO):
|
||||
return useShortName ? "LongT" : "LongTurbo";
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
|
||||
case PRESET(LONG_MODERATE):
|
||||
return useShortName ? "LongM" : "LongMod";
|
||||
break;
|
||||
case PRESET(LITE_FAST):
|
||||
return useShortName ? "LiteF" : "LiteFast";
|
||||
break;
|
||||
case PRESET(LITE_SLOW):
|
||||
return useShortName ? "LiteS" : "LiteSlow";
|
||||
break;
|
||||
case PRESET(NARROW_FAST):
|
||||
return useShortName ? "NarF" : "NarrowFast";
|
||||
break;
|
||||
case PRESET(NARROW_SLOW):
|
||||
return useShortName ? "NarS" : "NarrowSlow";
|
||||
break;
|
||||
case PRESET(TINY_FAST):
|
||||
return useShortName ? "TinyF" : "TinyFast";
|
||||
break;
|
||||
case PRESET(TINY_SLOW):
|
||||
return useShortName ? "TinyS" : "TinySlow";
|
||||
break;
|
||||
default:
|
||||
return useShortName ? "Custom" : "Invalid";
|
||||
break;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#if HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
|
||||
#include "FSCommon.h"
|
||||
#include "MessageStore.h"
|
||||
#include "NodeDB.h"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#if HAS_SCREEN
|
||||
#if HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
|
||||
|
||||
// Disable debug logging entirely on release builds of HELTEC_MESH_SOLAR for space constraints
|
||||
#if defined(HELTEC_MESH_SOLAR)
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* For more information, see: https://meshtastic.org/
|
||||
*/
|
||||
#include "power.h"
|
||||
#include "BluetoothCommon.h"
|
||||
#include "MessageStore.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
@@ -962,6 +963,10 @@ void Power::readPowerStatus()
|
||||
lastLogTime = millis();
|
||||
}
|
||||
newStatus.notifyObservers(&powerStatus2);
|
||||
|
||||
// Mirror battery level to the BLE Battery Service (0x2A19); the platform layer clamps and dedupes.
|
||||
if (hasBattery == OptTrue)
|
||||
updateBatteryLevel(powerStatus2.getBatteryChargePercent());
|
||||
#ifdef DEBUG_HEAP
|
||||
if (lastheap != memGet.getFreeHeap()) {
|
||||
// Use stack-allocated buffer to avoid heap allocations in monitoring code
|
||||
|
||||
+11
-1
@@ -1,6 +1,16 @@
|
||||
// TODO refactor this out with better radio configuration system
|
||||
#ifdef USE_RF95
|
||||
|
||||
#ifndef RF95_RESET
|
||||
#define RF95_RESET LORA_RESET
|
||||
#define RF95_IRQ LORA_DIO0 // on SX1262 version this is a no connect DIO0
|
||||
#endif
|
||||
|
||||
#ifndef RF95_IRQ
|
||||
#define RF95_IRQ LORA_DIO0 // on SX1262 version this is a no connect DIO0
|
||||
#endif
|
||||
|
||||
#ifndef RF95_DIO1
|
||||
#define RF95_DIO1 LORA_DIO1 // Note: not really used for RF95, but used for pure SX127x
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -51,7 +51,7 @@ size_t RedirectablePrint::write(uint8_t c)
|
||||
size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_list arg)
|
||||
{
|
||||
va_list copy;
|
||||
#if ENABLE_JSON_LOGGING || ARCH_PORTDUINO
|
||||
#if ARCH_PORTDUINO
|
||||
static char printBuf[512];
|
||||
#else
|
||||
static char printBuf[160];
|
||||
|
||||
+4
-3
@@ -133,11 +133,12 @@ bool AirTime::isTxAllowedChannelUtil(bool polite)
|
||||
|
||||
bool AirTime::isTxAllowedAirUtil()
|
||||
{
|
||||
if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) {
|
||||
if (utilizationTXPercent() < myRegion->dutyCycle * polite_duty_cycle_percent / 100) {
|
||||
float effectiveDutyCycle = getEffectiveDutyCycle();
|
||||
if (!config.lora.override_duty_cycle && effectiveDutyCycle < 100) {
|
||||
if (utilizationTXPercent() < effectiveDutyCycle * polite_duty_cycle_percent / 100) {
|
||||
return true;
|
||||
} else {
|
||||
LOG_WARN("TX air util. >%f%%. Skip send", myRegion->dutyCycle * polite_duty_cycle_percent / 100);
|
||||
LOG_WARN("TX air util. >%f%%. Skip send", effectiveDutyCycle * polite_duty_cycle_percent / 100);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#endif
|
||||
#if __has_include("SensorRtcHelper.hpp")
|
||||
#include "SensorRtcHelper.hpp"
|
||||
// SensorLib defines isBitSet as a macro; undefine it here to avoid conflicts
|
||||
// with the SparkFun MMC5983MA library, which has a class method of the same name.
|
||||
#ifdef isBitSet
|
||||
#undef isBitSet
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Offer chance for variant-specific defines */
|
||||
@@ -243,6 +248,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define QMI8658_ADDR 0x6B
|
||||
#define QMC5883L_ADDR 0x0D
|
||||
#define HMC5883L_ADDR 0x1E
|
||||
#define MMC5983MA_ADDR 0x30
|
||||
#define SHTC3_ADDR 0x70
|
||||
#define LPS22HB_ADDR 0x5C
|
||||
#define LPS22HB_ADDR_ALT 0x5D
|
||||
@@ -292,6 +298,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define DA217_ADDR 0x26
|
||||
#define BMI270_ADDR 0x68
|
||||
#define BMI270_ADDR_ALT 0x69
|
||||
#define ICM42607P_ADDR 0x68
|
||||
#define ICM42607P_ADDR_ALT 0x69
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// LED
|
||||
|
||||
@@ -37,8 +37,15 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
|
||||
{
|
||||
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150, BMI270};
|
||||
return firstOfOrNONE(10, types);
|
||||
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX,
|
||||
ICM20948, QMA6100P, BMM150, BMI270, ICM42607P};
|
||||
return firstOfOrNONE(11, types);
|
||||
}
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const
|
||||
{
|
||||
ScanI2C::DeviceType types[] = {MMC5983MA};
|
||||
return firstOfOrNONE(1, types);
|
||||
}
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstAQI() const
|
||||
|
||||
@@ -41,6 +41,7 @@ class ScanI2C
|
||||
QMI8658,
|
||||
QMC5883L,
|
||||
HMC5883L,
|
||||
MMC5983MA,
|
||||
PMSA003I,
|
||||
QMA6100P,
|
||||
MPU6050,
|
||||
@@ -65,6 +66,7 @@ class ScanI2C
|
||||
FT6336U,
|
||||
STK8BAXX,
|
||||
ICM20948,
|
||||
ICM42607P,
|
||||
SCD4X,
|
||||
MAX30102,
|
||||
TPS65233,
|
||||
@@ -149,6 +151,8 @@ class ScanI2C
|
||||
|
||||
FoundDevice firstAccelerometer() const;
|
||||
|
||||
FoundDevice firstMagnetometer() const;
|
||||
|
||||
FoundDevice firstAQI() const;
|
||||
|
||||
FoundDevice firstRGBLED() const;
|
||||
|
||||
@@ -179,8 +179,22 @@ String readSEN5xProductName(TwoWire *i2cBus, uint8_t address)
|
||||
#endif
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
static uint8_t crcSHT2X(const uint8_t *data, uint8_t len)
|
||||
{
|
||||
uint8_t crc = 0;
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (uint8_t bit = 0; bit < 8; bit++) {
|
||||
crc = (crc & 0x80) ? (crc << 1) ^ 0x31 : crc << 1;
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
bool detectSHT21SerialNumber(TwoWire *i2cBus, uint8_t address)
|
||||
{
|
||||
uint8_t serialA[8] = {0};
|
||||
uint8_t serialB[6] = {0};
|
||||
|
||||
i2cBus->beginTransmission(address);
|
||||
i2cBus->write(0xFA);
|
||||
@@ -189,12 +203,13 @@ bool detectSHT21SerialNumber(TwoWire *i2cBus, uint8_t address)
|
||||
if (i2cBus->endTransmission() != 0)
|
||||
return false;
|
||||
|
||||
if (i2cBus->requestFrom(address, (uint8_t)8) != 8)
|
||||
if (i2cBus->requestFrom(address, (uint8_t)sizeof(serialA)) != sizeof(serialA))
|
||||
return false;
|
||||
|
||||
// Just flush the data
|
||||
while (i2cBus->available() < 8) {
|
||||
i2cBus->read();
|
||||
for (uint8_t i = 0; i < sizeof(serialA); i++) {
|
||||
if (!i2cBus->available())
|
||||
return false;
|
||||
serialA[i] = i2cBus->read();
|
||||
}
|
||||
|
||||
i2cBus->beginTransmission(address);
|
||||
@@ -204,16 +219,18 @@ bool detectSHT21SerialNumber(TwoWire *i2cBus, uint8_t address)
|
||||
if (i2cBus->endTransmission() != 0)
|
||||
return false;
|
||||
|
||||
if (i2cBus->requestFrom(address, (uint8_t)6) != 6)
|
||||
if (i2cBus->requestFrom(address, (uint8_t)sizeof(serialB)) != sizeof(serialB))
|
||||
return false;
|
||||
|
||||
// Just flush the data
|
||||
while (i2cBus->available() < 6) {
|
||||
i2cBus->read();
|
||||
for (uint8_t i = 0; i < sizeof(serialB); i++) {
|
||||
if (!i2cBus->available())
|
||||
return false;
|
||||
serialB[i] = i2cBus->read();
|
||||
}
|
||||
|
||||
// Assume we detect the SHT21 if something came back from the request
|
||||
return true;
|
||||
return crcSHT2X(&serialA[0], 1) == serialA[1] && crcSHT2X(&serialA[2], 1) == serialA[3] &&
|
||||
crcSHT2X(&serialA[4], 1) == serialA[5] && crcSHT2X(&serialA[6], 1) == serialA[7] &&
|
||||
crcSHT2X(&serialB[0], 2) == serialB[2] && crcSHT2X(&serialB[3], 2) == serialB[5];
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -343,9 +360,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address);
|
||||
#ifdef HAS_NCP5623
|
||||
SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623", (uint8_t)addr.address);
|
||||
#endif
|
||||
#ifdef HAS_LP5562
|
||||
SCAN_SIMPLE_CASE(LP5562_ADDR, LP5562, "LP5562", (uint8_t)addr.address);
|
||||
#endif
|
||||
case XPOWERS_AXP192_AXP2101_ADDRESS:
|
||||
// Do we have the axp2101/192 or the TCA8418
|
||||
@@ -583,6 +597,18 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
#else
|
||||
SCAN_SIMPLE_CASE(PMSA003I_ADDR, PMSA003I, "PMSA003I", (uint8_t)addr.address)
|
||||
#endif
|
||||
case MMC5983MA_ADDR:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x2F), 1);
|
||||
if (registerValue == 0x30) {
|
||||
type = MMC5983MA;
|
||||
logFoundDevice("MMC5983MA", (uint8_t)addr.address);
|
||||
#ifdef HAS_LP5562
|
||||
} else {
|
||||
type = LP5562;
|
||||
logFoundDevice("LP5562", (uint8_t)addr.address);
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2);
|
||||
if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333
|
||||
@@ -737,8 +763,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
}
|
||||
break;
|
||||
|
||||
case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT, and SEN5X_ADDR
|
||||
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR
|
||||
case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT, ICM42607P_ADDR_ALT, and SEN5X_ADDR
|
||||
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR, and ICM42607P_ADDR
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
|
||||
#ifdef HAS_ICM20948
|
||||
type = ICM20948;
|
||||
@@ -758,6 +784,12 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
logFoundDevice("BMX160", (uint8_t)addr.address);
|
||||
break;
|
||||
} else {
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x75), 1); // WHO_AM_I
|
||||
if (registerValue == 0x60) {
|
||||
type = ICM42607P;
|
||||
logFoundDevice("ICM-42607-P", (uint8_t)addr.address);
|
||||
break;
|
||||
}
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
String prod = "";
|
||||
prod = readSEN5xProductName(i2cBus, addr.address);
|
||||
|
||||
+302
-17
@@ -17,7 +17,10 @@
|
||||
#include "main.h" // pmu_found
|
||||
#include "sleep.h"
|
||||
|
||||
#include "FSCommon.h"
|
||||
#include "GPSUpdateScheduling.h"
|
||||
#include "SPILock.h"
|
||||
#include "SafeFile.h"
|
||||
#include "cas.h"
|
||||
#include "ubx.h"
|
||||
|
||||
@@ -71,6 +74,67 @@ static struct uBloxGnssModelInfo {
|
||||
#define GPS_SOL_EXPIRY_MS 5000 // in millis. give 1 second time to combine different sentences. NMEA Frequency isn't higher anyway
|
||||
#define NMEA_MSG_GXGSA "GNGSA" // GSA message (GPGSA, GNGSA etc)
|
||||
|
||||
namespace
|
||||
{
|
||||
// Versioned on-disk record for persisted GPS probe results.
|
||||
constexpr uint32_t GPS_PROBE_CACHE_MAGIC = 0x47504348UL; // "GPCH"
|
||||
constexpr uint16_t GPS_PROBE_CACHE_VERSION = 1;
|
||||
constexpr const char *GPS_PROBE_CACHE_FILE = "/prefs/gps_probe_cache.dat";
|
||||
|
||||
struct GPSProbeCacheRecord {
|
||||
uint32_t magic;
|
||||
uint16_t version;
|
||||
uint16_t reserved;
|
||||
uint32_t baud;
|
||||
uint8_t model;
|
||||
};
|
||||
|
||||
bool isValidGnssModel(uint8_t model)
|
||||
{
|
||||
// Keep persisted values bounded to known enum range.
|
||||
return model <= static_cast<uint8_t>(GNSS_MODEL_CM121);
|
||||
}
|
||||
|
||||
bool isValidProbeBaud(uint32_t baud)
|
||||
{
|
||||
// Conservative sanity range for UART baud values.
|
||||
return baud >= 1200 && baud <= 921600;
|
||||
}
|
||||
|
||||
template <typename T> bool sawNmeaSentenceAtBaud(T *serialGps, uint32_t timeoutMs)
|
||||
{
|
||||
// Lightweight passive check: look for at least one complete
|
||||
// "$...,<field>\n" style NMEA sentence.
|
||||
const uint32_t deadline = millis() + timeoutMs;
|
||||
bool sawDollar = false;
|
||||
bool sawComma = false;
|
||||
|
||||
while ((int32_t)(millis() - deadline) < 0) {
|
||||
while (serialGps->available()) {
|
||||
char c = static_cast<char>(serialGps->read());
|
||||
if (c == '$') {
|
||||
sawDollar = true;
|
||||
sawComma = false;
|
||||
continue;
|
||||
}
|
||||
if (c == ',') {
|
||||
sawComma = true;
|
||||
}
|
||||
if (c == '\n' || c == '\r') {
|
||||
if (sawDollar && sawComma) {
|
||||
return true;
|
||||
}
|
||||
sawDollar = false;
|
||||
sawComma = false;
|
||||
}
|
||||
}
|
||||
delay(10);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// For logging
|
||||
static const char *getGPSPowerStateString(GPSPowerState state)
|
||||
{
|
||||
@@ -492,6 +556,201 @@ static const int rareSerialSpeeds[3] = {4800, 57600, GPS_BAUDRATE};
|
||||
#define GPS_PROBETRIES 2
|
||||
#endif
|
||||
|
||||
bool GPS::loadProbeCache()
|
||||
{
|
||||
#ifdef FSCom
|
||||
// Load the last known-good GPS model/baud pair so we can avoid a full probe
|
||||
// sweep on every boot.
|
||||
triedProbeCache = true; // Latch this boot's load attempt, even if no cache.
|
||||
GPSProbeCacheRecord record = {};
|
||||
size_t bytesRead = 0;
|
||||
|
||||
spiLock->lock();
|
||||
auto file = FSCom.open(GPS_PROBE_CACHE_FILE, FILE_O_READ);
|
||||
if (!file) {
|
||||
spiLock->unlock();
|
||||
return false;
|
||||
}
|
||||
bytesRead = file.read(reinterpret_cast<uint8_t *>(&record), sizeof(record));
|
||||
file.close();
|
||||
spiLock->unlock();
|
||||
|
||||
const bool headerValid = (bytesRead == sizeof(record)) && (record.magic == GPS_PROBE_CACHE_MAGIC) &&
|
||||
(record.version == GPS_PROBE_CACHE_VERSION) && (record.reserved == 0U);
|
||||
if (!headerValid || !isValidGnssModel(record.model) || !isValidProbeBaud(record.baud)) {
|
||||
clearProbeCache(); // Drop corrupt/invalid cache so next boot can
|
||||
// recover.
|
||||
return false;
|
||||
}
|
||||
|
||||
cachedProbeBaud = static_cast<int32_t>(record.baud);
|
||||
cachedProbeModel = static_cast<GnssModel_t>(record.model);
|
||||
hasProbeCache = true;
|
||||
triedProbeCache = false;
|
||||
LOG_INFO("Loaded cached GPS probe: baud=%u", record.baud);
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void GPS::clearProbeCache()
|
||||
{
|
||||
// Invalidate in-memory and on-disk cache so next boot is forced to do a
|
||||
// full probe.
|
||||
hasProbeCache = false;
|
||||
triedProbeCache = true;
|
||||
cachedProbeBaud = 0;
|
||||
cachedProbeModel = GNSS_MODEL_UNKNOWN;
|
||||
#ifdef FSCom
|
||||
spiLock->lock();
|
||||
if (FSCom.exists(GPS_PROBE_CACHE_FILE)) {
|
||||
FSCom.remove(GPS_PROBE_CACHE_FILE);
|
||||
}
|
||||
spiLock->unlock();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool GPS::saveProbeCache() const
|
||||
{
|
||||
#ifdef FSCom
|
||||
if (gnssModel == GNSS_MODEL_UNKNOWN || !isValidGnssModel(static_cast<uint8_t>(gnssModel)) ||
|
||||
!isValidProbeBaud(detectedBaud)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
spiLock->lock();
|
||||
FSCom.mkdir("/prefs");
|
||||
spiLock->unlock();
|
||||
GPSProbeCacheRecord record = {
|
||||
GPS_PROBE_CACHE_MAGIC, GPS_PROBE_CACHE_VERSION, 0, static_cast<uint32_t>(detectedBaud), static_cast<uint8_t>(gnssModel),
|
||||
};
|
||||
|
||||
auto file = SafeFile(GPS_PROBE_CACHE_FILE, true);
|
||||
spiLock->lock();
|
||||
const size_t written = file.write(reinterpret_cast<const uint8_t *>(&record), sizeof(record));
|
||||
spiLock->unlock();
|
||||
return (written == sizeof(record)) && file.close();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool GPS::verifyCachedProbePresence()
|
||||
{
|
||||
if (!hasProbeCache || cachedProbeModel == GNSS_MODEL_UNKNOWN || !isValidProbeBaud(cachedProbeBaud)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL)
|
||||
_serial_gps->end();
|
||||
_serial_gps->begin(cachedProbeBaud);
|
||||
#elif defined(ARCH_RP2040)
|
||||
_serial_gps->end();
|
||||
_serial_gps->setFIFOSize(256);
|
||||
_serial_gps->begin(cachedProbeBaud);
|
||||
#else
|
||||
if (_serial_gps->baudRate() != cachedProbeBaud) {
|
||||
LOG_DEBUG("Set GPS Baud to %i (cached verify)", cachedProbeBaud);
|
||||
_serial_gps->updateBaudRate(cachedProbeBaud);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Before trusting cached model/baud, require either active model-specific
|
||||
// response or passive NMEA flow.
|
||||
clearBuffer();
|
||||
bool present = false;
|
||||
|
||||
// Model-specific "active ping" checks to avoid false stale decisions on
|
||||
// modules that start streaming late.
|
||||
const char *cachedProbeModelName = "UNKNOWN";
|
||||
switch (cachedProbeModel) {
|
||||
case GNSS_MODEL_MTK:
|
||||
cachedProbeModelName = "L76K/MTK";
|
||||
_serial_gps->write("$PCAS06,0*1B\r\n");
|
||||
present = (getACK("$GPTXT,01,01,02,SW=", 700) == GNSS_RESPONSE_OK);
|
||||
break;
|
||||
case GNSS_MODEL_MTK_L76B:
|
||||
cachedProbeModelName = "L76B";
|
||||
case GNSS_MODEL_MTK_PA1010D:
|
||||
if (cachedProbeModel == GNSS_MODEL_MTK_PA1010D)
|
||||
cachedProbeModelName = "PA1010D";
|
||||
case GNSS_MODEL_MTK_PA1616S:
|
||||
if (cachedProbeModel == GNSS_MODEL_MTK_PA1616S)
|
||||
cachedProbeModelName = "PA1616S";
|
||||
case GNSS_MODEL_LS20031:
|
||||
if (cachedProbeModel == GNSS_MODEL_LS20031)
|
||||
cachedProbeModelName = "LS20031";
|
||||
_serial_gps->write("$PMTK605*31\r\n");
|
||||
present = (getACK("$PMTK705", 900) == GNSS_RESPONSE_OK);
|
||||
break;
|
||||
case GNSS_MODEL_AG3335:
|
||||
cachedProbeModelName = "AG3335";
|
||||
case GNSS_MODEL_AG3352:
|
||||
if (cachedProbeModel == GNSS_MODEL_AG3352)
|
||||
cachedProbeModelName = "AG3352";
|
||||
_serial_gps->write("$PAIR021*39\r\n");
|
||||
present = (getACK("$PAIR021,", 900) == GNSS_RESPONSE_OK);
|
||||
break;
|
||||
case GNSS_MODEL_ATGM336H:
|
||||
cachedProbeModelName = "ATGM336H";
|
||||
_serial_gps->write("$PCAS06,1*1A\r\n");
|
||||
present = (getACK("$GPTXT,01,01,02,HW=ATGM", 900) == GNSS_RESPONSE_OK);
|
||||
break;
|
||||
case GNSS_MODEL_UC6580:
|
||||
cachedProbeModelName = "UC6580/UM600";
|
||||
_serial_gps->write("$PDTINFO\r\n");
|
||||
present = (getACK("UC6580", 900) == GNSS_RESPONSE_OK) || (getACK("UM600", 900) == GNSS_RESPONSE_OK);
|
||||
break;
|
||||
case GNSS_MODEL_CM121:
|
||||
cachedProbeModelName = "CM121";
|
||||
_serial_gps->write("$PDTINFO\r\n");
|
||||
present = (getACK("CM121", 900) == GNSS_RESPONSE_OK);
|
||||
break;
|
||||
case GNSS_MODEL_UBLOX6:
|
||||
case GNSS_MODEL_UBLOX7:
|
||||
case GNSS_MODEL_UBLOX8:
|
||||
case GNSS_MODEL_UBLOX9:
|
||||
case GNSS_MODEL_UBLOX10: {
|
||||
if (cachedProbeModel == GNSS_MODEL_UBLOX6)
|
||||
cachedProbeModelName = "U-blox 6";
|
||||
else if (cachedProbeModel == GNSS_MODEL_UBLOX7)
|
||||
cachedProbeModelName = "U-blox 7";
|
||||
else if (cachedProbeModel == GNSS_MODEL_UBLOX8)
|
||||
cachedProbeModelName = "U-blox 8";
|
||||
else if (cachedProbeModel == GNSS_MODEL_UBLOX9)
|
||||
cachedProbeModelName = "U-blox 9";
|
||||
else if (cachedProbeModel == GNSS_MODEL_UBLOX10)
|
||||
cachedProbeModelName = "U-blox 10";
|
||||
|
||||
uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00};
|
||||
UBXChecksum(cfg_rate, sizeof(cfg_rate));
|
||||
_serial_gps->write(cfg_rate, sizeof(cfg_rate));
|
||||
present = (getACK(0x06, 0x08, 900) != GNSS_RESPONSE_NONE);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!present) {
|
||||
// Some modules may not respond to probes while still streaming NMEA, so
|
||||
// allow a passive fallback check.
|
||||
present = sawNmeaSentenceAtBaud(_serial_gps, 3000);
|
||||
}
|
||||
if (!present) {
|
||||
LOG_WARN("Cached GPS probe is stale (%s @ %d), clearing cache", cachedProbeModelName, cachedProbeBaud);
|
||||
clearProbeCache();
|
||||
cachedProbeFailedThisBoot = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
detectedBaud = cachedProbeBaud;
|
||||
gnssModel = cachedProbeModel;
|
||||
LOG_INFO("Using cached GPS probe: %s @ %d", cachedProbeModelName, detectedBaud);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Setup the GPS based on the model detected.
|
||||
* We detect the GPS by cycling through a set of baud rates, first common then rare.
|
||||
@@ -503,25 +762,46 @@ bool GPS::setup()
|
||||
{
|
||||
if (!didSerialInit) {
|
||||
int msglen = 0;
|
||||
if (cachedProbeFailedThisBoot) {
|
||||
// If cached verification failed, suppress further probing until
|
||||
// reboot.
|
||||
didSerialInit = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||
if (probeTries < GPS_PROBETRIES) {
|
||||
if (!hasProbeCache && !triedProbeCache) {
|
||||
(void)loadProbeCache();
|
||||
}
|
||||
|
||||
if (hasProbeCache && !triedProbeCache) {
|
||||
triedProbeCache = true;
|
||||
if (!verifyCachedProbePresence()) {
|
||||
// Cache was stale and got wiped; skip scanning this boot
|
||||
// and let next boot do a full probe.
|
||||
didSerialInit = true;
|
||||
return true;
|
||||
}
|
||||
} else if (probeTries < GPS_PROBETRIES) {
|
||||
// No usable cache: walk common baud rates first.
|
||||
gnssModel = probe(serialSpeeds[speedSelect]);
|
||||
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||
if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) {
|
||||
speedSelect = 0;
|
||||
++probeTries;
|
||||
}
|
||||
if (gnssModel != GNSS_MODEL_UNKNOWN) {
|
||||
detectedBaud = serialSpeeds[speedSelect];
|
||||
} else if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) {
|
||||
speedSelect = 0;
|
||||
++probeTries;
|
||||
}
|
||||
}
|
||||
// Rare Serial Speeds
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C6
|
||||
if (probeTries == GPS_PROBETRIES) {
|
||||
else if (probeTries == GPS_PROBETRIES) {
|
||||
// Then try less common baud rates before giving up.
|
||||
gnssModel = probe(rareSerialSpeeds[speedSelect]);
|
||||
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||
if (currentStep == 0 && ++speedSelect == array_count(rareSerialSpeeds)) {
|
||||
LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE);
|
||||
return true;
|
||||
}
|
||||
if (gnssModel != GNSS_MODEL_UNKNOWN) {
|
||||
detectedBaud = rareSerialSpeeds[speedSelect];
|
||||
} else if (currentStep == 0 && ++speedSelect == array_count(rareSerialSpeeds)) {
|
||||
LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -529,6 +809,7 @@ bool GPS::setup()
|
||||
|
||||
if (gnssModel != GNSS_MODEL_UNKNOWN) {
|
||||
setConnected();
|
||||
(void)saveProbeCache();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -1102,6 +1383,12 @@ int32_t GPS::runOnce()
|
||||
if (!setup())
|
||||
return currentDelay; // Setup failed, re-run in two seconds
|
||||
|
||||
if (cachedProbeFailedThisBoot || gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||
LOG_WARN("GPS not detected at cached settings; marked not present "
|
||||
"for this boot");
|
||||
return disable();
|
||||
}
|
||||
|
||||
// We have now loaded our saved preferences from flash
|
||||
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||
return disable();
|
||||
@@ -1304,7 +1591,7 @@ GnssModel_t GPS::probe(int serialSpeed)
|
||||
{"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335},
|
||||
{"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352},
|
||||
{"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352},
|
||||
{"UC6580", "UC6580", GNSS_MODEL_UC6580}
|
||||
{"UC6580", "UC6580", GNSS_MODEL_UC6580},
|
||||
// as L76K is sort of a last ditch effort, we won't attempt to detect it by startup messages for now.
|
||||
/*{"L76K", "SW=URANUS", GNSS_MODEL_MTK}*/};
|
||||
GnssModel_t detectedDriver = getProbeResponse(500, passive_detect, serialSpeed);
|
||||
@@ -1331,10 +1618,8 @@ GnssModel_t GPS::probe(int serialSpeed)
|
||||
case 1: {
|
||||
|
||||
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A,or CM121
|
||||
std::vector<ChipInfo> unicore = {{"UC6580", "UC6580", GNSS_MODEL_UC6580},
|
||||
{"UM600", "UM600", GNSS_MODEL_UC6580},
|
||||
{"CM121", "CM121", GNSS_MODEL_CM121},
|
||||
{"CC1167Q", "CC1167Q", GNSS_MODEL_CM121}};
|
||||
std::vector<ChipInfo> unicore = {
|
||||
{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}, {"CM121", "CM121", GNSS_MODEL_CM121}};
|
||||
PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500);
|
||||
currentDelay = 20;
|
||||
currentStep = 2;
|
||||
|
||||
@@ -155,8 +155,19 @@ class GPS : private concurrency::OSThread
|
||||
* @return true if we've acquired a new location
|
||||
*/
|
||||
virtual bool lookForLocation();
|
||||
// Load persisted GPS model+baud from /prefs.
|
||||
bool loadProbeCache();
|
||||
// Clear persisted GPS model+baud cache.
|
||||
void clearProbeCache();
|
||||
// Persist the currently detected GPS model+baud.
|
||||
bool saveProbeCache() const;
|
||||
// Verify the cached model+baud still maps to a live GPS device.
|
||||
bool verifyCachedProbePresence();
|
||||
|
||||
GnssModel_t gnssModel = GNSS_MODEL_UNKNOWN;
|
||||
int32_t detectedBaud = GPS_BAUDRATE;
|
||||
int32_t cachedProbeBaud = 0;
|
||||
GnssModel_t cachedProbeModel = GNSS_MODEL_UNKNOWN;
|
||||
|
||||
TinyGPSPlus reader;
|
||||
uint8_t fixQual = 0; // fix quality from GPGGA
|
||||
@@ -178,6 +189,12 @@ class GPS : private concurrency::OSThread
|
||||
|
||||
uint8_t speedSelect = 0;
|
||||
uint8_t probeTries = 0;
|
||||
// Cache file is successfully loaded.
|
||||
bool hasProbeCache = false;
|
||||
// Ensures cached probe is attempted once per boot.
|
||||
bool triedProbeCache = false;
|
||||
// Latched when cached presence check fails
|
||||
bool cachedProbeFailedThisBoot = false;
|
||||
|
||||
/**
|
||||
* hasValidLocation - indicates that the position variables contain a complete
|
||||
|
||||
+233
-109
@@ -50,6 +50,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "MeshService.h"
|
||||
#include "MessageStore.h"
|
||||
#include "RadioLibInterface.h"
|
||||
#include "SPILock.h"
|
||||
#include "error.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "gps/RTC.h"
|
||||
@@ -101,8 +102,8 @@ namespace graphics
|
||||
// if defined a pixel will blink to show redraws
|
||||
// #define SHOW_REDRAWS
|
||||
#define ASCII_BELL '\x07'
|
||||
// A text message frame + debug frame + all the node infos
|
||||
FrameCallback *normalFrames;
|
||||
// Base frames plus active module/favorite frames; grows only when the actual frameset needs it.
|
||||
static std::vector<FrameCallback> normalFrames;
|
||||
static uint32_t targetFramerate = IDLE_FRAMERATE;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
static inline void prepareFrameColorRegions()
|
||||
@@ -135,6 +136,7 @@ uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100};
|
||||
// At some point, we're going to ask all of the modules if they would like to display a screen frame
|
||||
// we'll need to hold onto pointers for the modules that can draw a frame.
|
||||
std::vector<MeshModule *> moduleFrames;
|
||||
static uint8_t moduleFrameStart = 0;
|
||||
|
||||
#if HAS_GPS
|
||||
// GeoCoord object for the screen
|
||||
@@ -332,8 +334,14 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
||||
// otherwise, just display the module frame that's aligned with the current frame
|
||||
module_frame = state->currentFrame;
|
||||
}
|
||||
MeshModule &pi = *moduleFrames.at(module_frame);
|
||||
pi.drawFrame(display, state, x, y);
|
||||
if (module_frame < moduleFrameStart)
|
||||
return;
|
||||
|
||||
const uint8_t moduleIndex = module_frame - moduleFrameStart;
|
||||
if (moduleIndex >= moduleFrames.size() || moduleFrames[moduleIndex] == nullptr)
|
||||
return;
|
||||
|
||||
moduleFrames[moduleIndex]->drawFrame(display, state, x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,9 +409,11 @@ SPIClass SPI1(HSPI);
|
||||
#endif
|
||||
|
||||
Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_OledType screenType, OLEDDISPLAY_GEOMETRY geometry)
|
||||
: concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(32)
|
||||
: concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(16)
|
||||
{
|
||||
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
|
||||
normalFrames.reserve(24);
|
||||
indicatorIcons.reserve(24);
|
||||
moduleFrames.reserve(8);
|
||||
|
||||
#if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64)
|
||||
dispdev = new SH1106Wire(address.address, -1, -1, geometry,
|
||||
@@ -487,7 +497,6 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
|
||||
Screen::~Screen()
|
||||
{
|
||||
delete[] graphics::normalFrames;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -655,6 +664,10 @@ void Screen::setup()
|
||||
brightness = uiconfig.screen_brightness;
|
||||
}
|
||||
|
||||
// Restore which frames the user has hidden (persisted across reboots).
|
||||
// Must happen before the first setFrames().
|
||||
loadFrameVisibility();
|
||||
|
||||
// Detect OLED subtype (if supported by board variant)
|
||||
#ifdef AutoOLEDWire_h
|
||||
if (isAUTOOled)
|
||||
@@ -1137,183 +1150,130 @@ void Screen::setFrames(FrameFocus focus)
|
||||
LOG_DEBUG("Show standard frames");
|
||||
showingNormalScreen = true;
|
||||
|
||||
normalFrames.clear();
|
||||
indicatorIcons.clear();
|
||||
|
||||
size_t numframes = 0;
|
||||
auto appendFrame = [this](FrameCallback frame, const uint8_t *icon) -> uint8_t {
|
||||
normalFrames.emplace_back(frame);
|
||||
indicatorIcons.push_back(icon);
|
||||
return normalFrames.size() - 1;
|
||||
};
|
||||
|
||||
// If we have a critical fault, show it first
|
||||
fsi.positions.fault = numframes;
|
||||
fsi.positions.fault = normalFrames.size();
|
||||
if (error_code) {
|
||||
normalFrames[numframes++] = NotificationRenderer::drawCriticalFaultFrame;
|
||||
indicatorIcons.push_back(icon_error);
|
||||
fsi.positions.fault = appendFrame(NotificationRenderer::drawCriticalFaultFrame, icon_error);
|
||||
focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame
|
||||
}
|
||||
|
||||
#if defined(DISPLAY_CLOCK_FRAME)
|
||||
if (!hiddenFrames.clock) {
|
||||
fsi.positions.clock = numframes;
|
||||
#if defined(OLED_TINY)
|
||||
normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame;
|
||||
fsi.positions.clock = appendFrame(graphics::ClockRenderer::drawAnalogClockFrame, digital_icon_clock);
|
||||
#else
|
||||
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
|
||||
: graphics::ClockRenderer::drawDigitalClockFrame;
|
||||
fsi.positions.clock = appendFrame(uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
|
||||
: graphics::ClockRenderer::drawDigitalClockFrame,
|
||||
digital_icon_clock);
|
||||
#endif
|
||||
indicatorIcons.push_back(digital_icon_clock);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!hiddenFrames.home) {
|
||||
fsi.positions.home = numframes;
|
||||
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
|
||||
indicatorIcons.push_back(icon_home);
|
||||
fsi.positions.home = appendFrame(graphics::UIRenderer::drawDeviceFocused, icon_home);
|
||||
}
|
||||
|
||||
fsi.positions.textMessage = numframes;
|
||||
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
|
||||
indicatorIcons.push_back(icon_mail);
|
||||
fsi.positions.textMessage = appendFrame(graphics::MessageRenderer::drawTextMessageFrame, icon_mail);
|
||||
|
||||
#ifndef USE_EINK
|
||||
if (!hiddenFrames.nodelist_nodes) {
|
||||
fsi.positions.nodelist_nodes = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes;
|
||||
indicatorIcons.push_back(icon_nodes);
|
||||
fsi.positions.nodelist_nodes = appendFrame(graphics::NodeListRenderer::drawDynamicListScreen_Nodes, icon_nodes);
|
||||
}
|
||||
if (!hiddenFrames.nodelist_location) {
|
||||
fsi.positions.nodelist_location = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location;
|
||||
indicatorIcons.push_back(icon_list);
|
||||
fsi.positions.nodelist_location = appendFrame(graphics::NodeListRenderer::drawDynamicListScreen_Location, icon_list);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Show detailed node views only on E-Ink builds
|
||||
#ifdef USE_EINK
|
||||
if (!hiddenFrames.nodelist_lastheard) {
|
||||
fsi.positions.nodelist_lastheard = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
|
||||
indicatorIcons.push_back(icon_nodes);
|
||||
fsi.positions.nodelist_lastheard = appendFrame(graphics::NodeListRenderer::drawLastHeardScreen, icon_nodes);
|
||||
}
|
||||
if (!hiddenFrames.nodelist_hopsignal) {
|
||||
fsi.positions.nodelist_hopsignal = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
|
||||
indicatorIcons.push_back(icon_signal);
|
||||
fsi.positions.nodelist_hopsignal = appendFrame(graphics::NodeListRenderer::drawHopSignalScreen, icon_signal);
|
||||
}
|
||||
if (!hiddenFrames.nodelist_distance) {
|
||||
fsi.positions.nodelist_distance = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
|
||||
indicatorIcons.push_back(icon_distance);
|
||||
fsi.positions.nodelist_distance = appendFrame(graphics::NodeListRenderer::drawDistanceScreen, icon_distance);
|
||||
}
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
#ifdef USE_EINK
|
||||
if (!hiddenFrames.nodelist_bearings) {
|
||||
fsi.positions.nodelist_bearings = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
|
||||
indicatorIcons.push_back(icon_list);
|
||||
fsi.positions.nodelist_bearings = appendFrame(graphics::NodeListRenderer::drawNodeListWithCompasses, icon_list);
|
||||
}
|
||||
#endif
|
||||
if (!hiddenFrames.gps) {
|
||||
fsi.positions.gps = numframes;
|
||||
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
|
||||
indicatorIcons.push_back(icon_compass);
|
||||
fsi.positions.gps = appendFrame(graphics::UIRenderer::drawCompassAndLocationScreen, icon_compass);
|
||||
}
|
||||
#endif
|
||||
if (RadioLibInterface::instance && !hiddenFrames.lora) {
|
||||
fsi.positions.lora = numframes;
|
||||
normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused;
|
||||
indicatorIcons.push_back(icon_radio);
|
||||
fsi.positions.lora = appendFrame(graphics::DebugRenderer::drawLoRaFocused, icon_radio);
|
||||
}
|
||||
if (!hiddenFrames.system) {
|
||||
fsi.positions.system = numframes;
|
||||
normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen;
|
||||
indicatorIcons.push_back(icon_system);
|
||||
fsi.positions.system = appendFrame(graphics::DebugRenderer::drawSystemScreen, icon_system);
|
||||
}
|
||||
#if !defined(DISPLAY_CLOCK_FRAME)
|
||||
if (!hiddenFrames.clock) {
|
||||
fsi.positions.clock = numframes;
|
||||
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
|
||||
: graphics::ClockRenderer::drawDigitalClockFrame;
|
||||
indicatorIcons.push_back(digital_icon_clock);
|
||||
fsi.positions.clock = appendFrame(uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
|
||||
: graphics::ClockRenderer::drawDigitalClockFrame,
|
||||
digital_icon_clock);
|
||||
}
|
||||
#endif
|
||||
if (!hiddenFrames.chirpy) {
|
||||
fsi.positions.chirpy = numframes;
|
||||
normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy;
|
||||
indicatorIcons.push_back(chirpy_small);
|
||||
fsi.positions.chirpy = appendFrame(graphics::DebugRenderer::drawChirpy, chirpy_small);
|
||||
}
|
||||
|
||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||
if (!hiddenFrames.wifi && isWifiAvailable()) {
|
||||
fsi.positions.wifi = numframes;
|
||||
normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline;
|
||||
indicatorIcons.push_back(icon_wifi);
|
||||
fsi.positions.wifi = appendFrame(graphics::DebugRenderer::drawDebugInfoWiFiTrampoline, icon_wifi);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Beware of what changes you make in this code!
|
||||
// We pass numframes into GetMeshModulesWithUIFrames() which is highly important!
|
||||
// Inside of that callback, goes over to MeshModule.cpp and we run
|
||||
// modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr
|
||||
// entries until we're ready to start building the matching entries.
|
||||
// We are doing our best to keep the normalFrames vector
|
||||
// and the moduleFrames vector in lock step.
|
||||
moduleFrames = MeshModule::GetMeshModulesWithUIFrames(numframes);
|
||||
moduleFrameStart = normalFrames.size();
|
||||
moduleFrames = MeshModule::GetMeshModulesWithUIFrames();
|
||||
LOG_DEBUG("Show %d module frames", moduleFrames.size());
|
||||
|
||||
for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) {
|
||||
// Draw the module frame, using the hack described above
|
||||
if (*i != nullptr) {
|
||||
normalFrames[numframes] = drawModuleFrame;
|
||||
|
||||
// Check if the module being drawn has requested focus
|
||||
// We will honor this request later, if setFrames was triggered by a UIFrameEvent
|
||||
MeshModule *m = *i;
|
||||
if (m && m->isRequestingFocus())
|
||||
fsi.positions.focusedModule = numframes;
|
||||
if (m && m == waypointModule)
|
||||
fsi.positions.waypoint = numframes;
|
||||
|
||||
indicatorIcons.push_back(icon_module);
|
||||
numframes++;
|
||||
}
|
||||
for (MeshModule *m : moduleFrames) {
|
||||
const uint8_t frameIndex = appendFrame(drawModuleFrame, icon_module);
|
||||
if (m && m->isRequestingFocus())
|
||||
fsi.positions.focusedModule = frameIndex;
|
||||
if (m && m == waypointModule)
|
||||
fsi.positions.waypoint = frameIndex;
|
||||
}
|
||||
|
||||
LOG_DEBUG("Added modules. numframes: %d", numframes);
|
||||
|
||||
// We don't show the node info of our node (if we have it yet - we should)
|
||||
size_t numMeshNodes = nodeDB->getNumMeshNodes();
|
||||
if (numMeshNodes > 0)
|
||||
numMeshNodes--;
|
||||
LOG_DEBUG("Added modules. numframes: %d", normalFrames.size());
|
||||
|
||||
if (!hiddenFrames.show_favorites) {
|
||||
// Temporary array to hold favorite node frames
|
||||
std::vector<FrameCallback> favoriteFrames;
|
||||
|
||||
uint8_t firstFavorite = 255;
|
||||
uint8_t lastFavorite = 255;
|
||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||
if (n && n->num != nodeDB->getNodeNum() && nodeInfoLiteIsFavorite(n)) {
|
||||
favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode);
|
||||
const uint8_t frameIndex = appendFrame(graphics::UIRenderer::drawFavoriteNode, icon_node);
|
||||
if (firstFavorite == 255)
|
||||
firstFavorite = frameIndex;
|
||||
lastFavorite = frameIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert favorite frames *after* collecting them all
|
||||
if (!favoriteFrames.empty()) {
|
||||
fsi.positions.firstFavorite = numframes;
|
||||
for (const auto &f : favoriteFrames) {
|
||||
normalFrames[numframes++] = f;
|
||||
indicatorIcons.push_back(icon_node);
|
||||
}
|
||||
fsi.positions.lastFavorite = numframes - 1;
|
||||
} else {
|
||||
fsi.positions.firstFavorite = 255;
|
||||
fsi.positions.lastFavorite = 255;
|
||||
}
|
||||
fsi.positions.firstFavorite = firstFavorite;
|
||||
fsi.positions.lastFavorite = lastFavorite;
|
||||
}
|
||||
|
||||
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
|
||||
this->frameCount = numframes; // Save frame count for use in custom overlay
|
||||
LOG_DEBUG("Finished build frames. numframes: %d", numframes);
|
||||
fsi.frameCount = normalFrames.size(); // Total framecount is used to apply FOCUS_PRESERVE
|
||||
this->frameCount = normalFrames.size(); // Save frame count for use in custom overlay
|
||||
LOG_DEBUG("Finished build frames. numframes: %d", normalFrames.size());
|
||||
|
||||
ui->setFrames(normalFrames, numframes);
|
||||
ui->setFrames(normalFrames.data(), fsi.frameCount);
|
||||
ui->disableAllIndicators();
|
||||
|
||||
// Add overlays: frame icons and alert banner)
|
||||
@@ -1416,6 +1376,9 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
|
||||
if (frameName == "chirpy") {
|
||||
hiddenFrames.chirpy = !hiddenFrames.chirpy;
|
||||
}
|
||||
|
||||
// Save the new visibility state so it survives a reboot.
|
||||
saveFrameVisibility();
|
||||
}
|
||||
|
||||
bool Screen::isFrameHidden(const std::string &frameName) const
|
||||
@@ -1454,6 +1417,167 @@ bool Screen::isFrameHidden(const std::string &frameName) const
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Frame visibility persistence
|
||||
//
|
||||
// The set of hideable frames varies by build (USE_EINK, HAS_GPS, ...), so we
|
||||
// serialize to a fixed bitmask where each frame name owns a permanent bit
|
||||
// position. Bits for frames that don't exist in the current build are simply
|
||||
// left untouched, which keeps the saved file portable across firmware variants.
|
||||
// ---------------------------------------------------------------------------
|
||||
namespace
|
||||
{
|
||||
static const char *frameVisibilityFileName = "/prefs/framevis";
|
||||
constexpr uint32_t FRAMEVIS_MAGIC = 0x53495646; // "FVIS" little-endian
|
||||
constexpr uint8_t FRAMEVIS_VERSION = 1;
|
||||
|
||||
// Permanent bit assignments. Never renumber these; only append new ones.
|
||||
enum FrameVisBit : uint8_t {
|
||||
FVBIT_TEXT_MESSAGE = 0,
|
||||
FVBIT_WAYPOINT = 1,
|
||||
FVBIT_WIFI = 2,
|
||||
FVBIT_SYSTEM = 3,
|
||||
FVBIT_HOME = 4,
|
||||
FVBIT_CLOCK = 5,
|
||||
FVBIT_NODELIST_NODES = 6,
|
||||
FVBIT_NODELIST_LOCATION = 7,
|
||||
FVBIT_NODELIST_LASTHEARD = 8,
|
||||
FVBIT_NODELIST_HOPSIGNAL = 9,
|
||||
FVBIT_NODELIST_DISTANCE = 10,
|
||||
FVBIT_NODELIST_BEARINGS = 11,
|
||||
FVBIT_GPS = 12,
|
||||
FVBIT_LORA = 13,
|
||||
FVBIT_SHOW_FAVORITES = 14,
|
||||
FVBIT_CHIRPY = 15,
|
||||
};
|
||||
|
||||
struct __attribute__((packed)) FrameVisFile {
|
||||
uint32_t magic;
|
||||
uint8_t version;
|
||||
uint32_t mask;
|
||||
};
|
||||
|
||||
inline void setBit(uint32_t &mask, uint8_t bit, bool value)
|
||||
{
|
||||
if (value)
|
||||
mask |= (1UL << bit);
|
||||
else
|
||||
mask &= ~(1UL << bit);
|
||||
}
|
||||
|
||||
inline bool getBit(uint32_t mask, uint8_t bit)
|
||||
{
|
||||
return (mask & (1UL << bit)) != 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
uint32_t Screen::packHiddenFrames() const
|
||||
{
|
||||
uint32_t mask = 0;
|
||||
setBit(mask, FVBIT_TEXT_MESSAGE, hiddenFrames.textMessage);
|
||||
setBit(mask, FVBIT_WAYPOINT, hiddenFrames.waypoint);
|
||||
setBit(mask, FVBIT_WIFI, hiddenFrames.wifi);
|
||||
setBit(mask, FVBIT_SYSTEM, hiddenFrames.system);
|
||||
setBit(mask, FVBIT_HOME, hiddenFrames.home);
|
||||
setBit(mask, FVBIT_CLOCK, hiddenFrames.clock);
|
||||
#ifndef USE_EINK
|
||||
setBit(mask, FVBIT_NODELIST_NODES, hiddenFrames.nodelist_nodes);
|
||||
setBit(mask, FVBIT_NODELIST_LOCATION, hiddenFrames.nodelist_location);
|
||||
#endif
|
||||
#ifdef USE_EINK
|
||||
setBit(mask, FVBIT_NODELIST_LASTHEARD, hiddenFrames.nodelist_lastheard);
|
||||
setBit(mask, FVBIT_NODELIST_HOPSIGNAL, hiddenFrames.nodelist_hopsignal);
|
||||
setBit(mask, FVBIT_NODELIST_DISTANCE, hiddenFrames.nodelist_distance);
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
#ifdef USE_EINK
|
||||
setBit(mask, FVBIT_NODELIST_BEARINGS, hiddenFrames.nodelist_bearings);
|
||||
#endif
|
||||
setBit(mask, FVBIT_GPS, hiddenFrames.gps);
|
||||
#endif
|
||||
setBit(mask, FVBIT_LORA, hiddenFrames.lora);
|
||||
setBit(mask, FVBIT_SHOW_FAVORITES, hiddenFrames.show_favorites);
|
||||
setBit(mask, FVBIT_CHIRPY, hiddenFrames.chirpy);
|
||||
return mask;
|
||||
}
|
||||
|
||||
void Screen::applyHiddenFramesMask(uint32_t mask)
|
||||
{
|
||||
hiddenFrames.textMessage = getBit(mask, FVBIT_TEXT_MESSAGE);
|
||||
hiddenFrames.waypoint = getBit(mask, FVBIT_WAYPOINT);
|
||||
hiddenFrames.wifi = getBit(mask, FVBIT_WIFI);
|
||||
hiddenFrames.system = getBit(mask, FVBIT_SYSTEM);
|
||||
hiddenFrames.home = getBit(mask, FVBIT_HOME);
|
||||
hiddenFrames.clock = getBit(mask, FVBIT_CLOCK);
|
||||
#ifndef USE_EINK
|
||||
hiddenFrames.nodelist_nodes = getBit(mask, FVBIT_NODELIST_NODES);
|
||||
hiddenFrames.nodelist_location = getBit(mask, FVBIT_NODELIST_LOCATION);
|
||||
#endif
|
||||
#ifdef USE_EINK
|
||||
hiddenFrames.nodelist_lastheard = getBit(mask, FVBIT_NODELIST_LASTHEARD);
|
||||
hiddenFrames.nodelist_hopsignal = getBit(mask, FVBIT_NODELIST_HOPSIGNAL);
|
||||
hiddenFrames.nodelist_distance = getBit(mask, FVBIT_NODELIST_DISTANCE);
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
#ifdef USE_EINK
|
||||
hiddenFrames.nodelist_bearings = getBit(mask, FVBIT_NODELIST_BEARINGS);
|
||||
#endif
|
||||
hiddenFrames.gps = getBit(mask, FVBIT_GPS);
|
||||
#endif
|
||||
hiddenFrames.lora = getBit(mask, FVBIT_LORA);
|
||||
hiddenFrames.show_favorites = getBit(mask, FVBIT_SHOW_FAVORITES);
|
||||
hiddenFrames.chirpy = getBit(mask, FVBIT_CHIRPY);
|
||||
}
|
||||
|
||||
void Screen::loadFrameVisibility()
|
||||
{
|
||||
#ifdef FSCom
|
||||
spiLock->lock();
|
||||
auto file = FSCom.open(frameVisibilityFileName, FILE_O_READ);
|
||||
if (file) {
|
||||
FrameVisFile data{};
|
||||
bool ok = file.read((uint8_t *)&data, sizeof(data)) == sizeof(data) && data.magic == FRAMEVIS_MAGIC &&
|
||||
data.version == FRAMEVIS_VERSION;
|
||||
file.close();
|
||||
spiLock->unlock();
|
||||
if (ok) {
|
||||
applyHiddenFramesMask(data.mask);
|
||||
LOG_INFO("Loaded frame visibility (mask 0x%08x)", data.mask);
|
||||
} else {
|
||||
LOG_WARN("Frame visibility file invalid, keeping defaults");
|
||||
}
|
||||
return;
|
||||
}
|
||||
spiLock->unlock();
|
||||
LOG_DEBUG("No saved frame visibility, using defaults");
|
||||
#endif
|
||||
}
|
||||
|
||||
void Screen::saveFrameVisibility()
|
||||
{
|
||||
#ifdef FSCom
|
||||
spiLock->lock();
|
||||
FSCom.mkdir("/prefs");
|
||||
if (FSCom.exists(frameVisibilityFileName))
|
||||
FSCom.remove(frameVisibilityFileName);
|
||||
|
||||
auto file = FSCom.open(frameVisibilityFileName, FILE_O_WRITE);
|
||||
if (file) {
|
||||
FrameVisFile data{};
|
||||
data.magic = FRAMEVIS_MAGIC;
|
||||
data.version = FRAMEVIS_VERSION;
|
||||
data.mask = packHiddenFrames();
|
||||
file.write((uint8_t *)&data, sizeof(data));
|
||||
file.flush();
|
||||
file.close();
|
||||
LOG_INFO("Saved frame visibility (mask 0x%08x)", data.mask);
|
||||
} else {
|
||||
LOG_WARN("Failed to open %s for writing", frameVisibilityFileName);
|
||||
}
|
||||
spiLock->unlock();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Screen::handleStartFirmwareUpdateScreen()
|
||||
{
|
||||
LOG_DEBUG("Show firmware screen");
|
||||
|
||||
+11
-1
@@ -12,7 +12,7 @@
|
||||
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
|
||||
namespace graphics
|
||||
{
|
||||
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input };
|
||||
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, hex_picker, text_input };
|
||||
|
||||
struct BannerOverlayOptions {
|
||||
const char *message;
|
||||
@@ -623,6 +623,11 @@ class Screen : public concurrency::OSThread
|
||||
void toggleFrameVisibility(const std::string &frameName);
|
||||
bool isFrameHidden(const std::string &frameName) const;
|
||||
|
||||
// Persist / restore which frames are hidden, across reboots.
|
||||
// Stored as a single uint32 bitmask in /prefs (see Screen.cpp for the format).
|
||||
void loadFrameVisibility();
|
||||
void saveFrameVisibility();
|
||||
|
||||
#ifdef USE_EINK
|
||||
/// Draw an image to remain on E-Ink display after screen off
|
||||
void setScreensaverFrames(FrameCallback einkScreensaver = NULL);
|
||||
@@ -738,6 +743,11 @@ class Screen : public concurrency::OSThread
|
||||
bool chirpy = true;
|
||||
} hiddenFrames;
|
||||
|
||||
// Convert hiddenFrames to a uint32 bitmask. Bit positions are fixed per
|
||||
// frame name (see Screen.cpp).
|
||||
uint32_t packHiddenFrames() const;
|
||||
void applyHiddenFramesMask(uint32_t mask);
|
||||
|
||||
/// Try to start drawing ASAP
|
||||
void setFastFramerate();
|
||||
|
||||
|
||||
@@ -578,7 +578,11 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
#endif
|
||||
|
||||
display->setColor(BLACK);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
display->fillRect(0, footerY, SCREEN_WIDTH, footerH);
|
||||
#else
|
||||
display->fillRect(0, footerY, connection_icon_width + 1, footerH);
|
||||
#endif
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||||
|
||||
@@ -1458,7 +1458,8 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON);
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
tft->displayOn();
|
||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096)
|
||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) && \
|
||||
!defined(HELTEC_MESH_NODE_T1)
|
||||
tft->wakeup();
|
||||
tft->powerSaveOff();
|
||||
#endif
|
||||
@@ -1469,7 +1470,7 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
#ifdef UNPHONE
|
||||
unphone.backlight(true); // using unPhone library
|
||||
#endif
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
|
||||
#elif !defined(M5STACK) && !defined(ST7789_CS) && \
|
||||
!defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function
|
||||
tft->setBrightness(172);
|
||||
@@ -1485,7 +1486,8 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON);
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
tft->displayOff();
|
||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096)
|
||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) && \
|
||||
!defined(HELTEC_MESH_NODE_T1)
|
||||
tft->sleep();
|
||||
tft->powerSaveOn();
|
||||
#endif
|
||||
@@ -1496,7 +1498,7 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
#ifdef UNPHONE
|
||||
unphone.backlight(false); // using unPhone library
|
||||
#endif
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
|
||||
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR)
|
||||
tft->setBrightness(0);
|
||||
#endif
|
||||
@@ -1511,7 +1513,7 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
|
||||
void TFTDisplay::setDisplayBrightness(uint8_t _brightness)
|
||||
{
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
|
||||
// todo
|
||||
#elif !defined(HACKADAY_COMMUNICATOR)
|
||||
tft->setBrightness(_brightness);
|
||||
@@ -1531,7 +1533,8 @@ bool TFTDisplay::hasTouch(void)
|
||||
{
|
||||
#ifdef RAK14014
|
||||
return true;
|
||||
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096)
|
||||
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && \
|
||||
!defined(HELTEC_MESH_NODE_T1)
|
||||
return tft->touch() != nullptr;
|
||||
#else
|
||||
return false;
|
||||
@@ -1550,7 +1553,8 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y)
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096)
|
||||
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && \
|
||||
!defined(HELTEC_MESH_NODE_T1)
|
||||
return tft->getTouch(x, y);
|
||||
#else
|
||||
return false;
|
||||
@@ -1567,7 +1571,7 @@ bool TFTDisplay::connect()
|
||||
{
|
||||
concurrency::LockGuard g(spiLock);
|
||||
LOG_INFO("Do TFT init");
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
|
||||
tft = new TFT_eSPI;
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
|
||||
|
||||
@@ -142,8 +142,9 @@ void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offset
|
||||
if (keyboardStartY < 0)
|
||||
keyboardStartY = 0;
|
||||
} else {
|
||||
// Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom
|
||||
cellH = KEY_HEIGHT;
|
||||
// Default (non-wide, non-64px) e.g. SH1107 128x128:
|
||||
// cellH = FONT_HEIGHT_SMALL - 2 so rows are tighter while still hosting the font
|
||||
cellH = std::max((int)KEY_HEIGHT, FONT_HEIGHT_SMALL - 2);
|
||||
int keyboardHeight = KEYBOARD_ROWS * cellH;
|
||||
keyboardStartY = screenH - keyboardHeight;
|
||||
if (keyboardStartY < 0)
|
||||
@@ -446,11 +447,8 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool
|
||||
if (textX < x)
|
||||
textX = x; // guard
|
||||
} else {
|
||||
if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) {
|
||||
textX = x + (width - textWidth + 1) / 2;
|
||||
} else {
|
||||
textX = x + (width - textWidth) / 2;
|
||||
}
|
||||
// Use ceil rounding for all screens (consistent with 128x64 behavior for numbers)
|
||||
textX = x + (width - textWidth + 1) / 2;
|
||||
}
|
||||
int contentTop = y;
|
||||
int contentH = height;
|
||||
@@ -746,4 +744,4 @@ bool VirtualKeyboard::isTimedOut() const
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -183,9 +183,13 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
static float segmentHeight = SEGMENT_HEIGHT * 0.75f;
|
||||
|
||||
if (!scaleInitialized) {
|
||||
#ifdef DISPLAY_FORCE_SMALL_FONTS
|
||||
float screenwidth_target_ratio = 0.70f; // Target 70% of display width (adjustable)
|
||||
#else
|
||||
float screenwidth_target_ratio = 0.80f; // Target 80% of display width (adjustable)
|
||||
float max_scale = 3.5f; // Safety limit to avoid runaway scaling
|
||||
float step = 0.05f; // Step increment per iteration
|
||||
#endif
|
||||
float max_scale = 3.5f; // Safety limit to avoid runaway scaling
|
||||
float step = 0.05f; // Step increment per iteration
|
||||
|
||||
float target_width = display->getWidth() * screenwidth_target_ratio;
|
||||
float target_height =
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#if HAS_SCREEN
|
||||
#include "ClockRenderer.h"
|
||||
#include "Default.h"
|
||||
#include "DisplayFormatters.h"
|
||||
#include "GPS.h"
|
||||
#include "MenuHandler.h"
|
||||
#include "MeshRadio.h"
|
||||
@@ -125,6 +126,7 @@ void launchReplyForMessage(const StoredMessage &message, bool freetext)
|
||||
|
||||
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
|
||||
uint32_t menuHandler::pickedNodeNum = 0;
|
||||
meshtastic_Config_LoRaConfig_RegionCode menuHandler::pendingRegion = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
||||
bool test_enabled = false;
|
||||
uint8_t test_count = 0;
|
||||
|
||||
@@ -173,6 +175,36 @@ void menuHandler::OnboardMessage()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
static void applyLoraRegion(meshtastic_Config_LoRaConfig_RegionCode region, bool isHam)
|
||||
{
|
||||
config.lora.region = region;
|
||||
config.lora.channel_num = 0; // Reset to default channel
|
||||
|
||||
if (isHam && adminModule) {
|
||||
meshtastic_HamParameters hamParams = meshtastic_HamParameters_init_zero;
|
||||
strncpy(hamParams.call_sign, "N0CALL", sizeof(hamParams.call_sign) - 1);
|
||||
strncpy(hamParams.short_name, "N0CL", sizeof(hamParams.short_name));
|
||||
hamParams.tx_power = config.lora.tx_power;
|
||||
hamParams.frequency = config.lora.override_frequency;
|
||||
adminModule->handleSetHamMode(hamParams);
|
||||
}
|
||||
auto changes = SEGMENT_CONFIG;
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
||||
if (crypto) {
|
||||
crypto->ensurePkiKeys(config.security, owner);
|
||||
}
|
||||
#endif
|
||||
initRegion();
|
||||
if (getEffectiveDutyCycle() < 100) {
|
||||
config.lora.ignore_mqtt = true;
|
||||
}
|
||||
if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) {
|
||||
sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name);
|
||||
changes |= SEGMENT_MODULECONFIG;
|
||||
}
|
||||
service->reloadConfig(changes);
|
||||
}
|
||||
|
||||
void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
{
|
||||
static const LoraRegionOption regionOptions[] = {
|
||||
@@ -180,6 +212,8 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
{"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US},
|
||||
{"EU_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_433},
|
||||
{"EU_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_868},
|
||||
{"EU_866", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_866},
|
||||
{"EU_868_NARROW", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_N_868},
|
||||
{"CN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_CN},
|
||||
{"JP", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_JP},
|
||||
{"ANZ", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ},
|
||||
@@ -203,6 +237,11 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
{"KZ_863", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_863},
|
||||
{"NP_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NP_865},
|
||||
{"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902},
|
||||
{"ITU1_2M (144-146)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M},
|
||||
{"ITU2_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU2_2M},
|
||||
{"ITU3_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M},
|
||||
{"ITU2_125CM (220-225)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM},
|
||||
|
||||
};
|
||||
|
||||
constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]);
|
||||
@@ -234,27 +273,20 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
return;
|
||||
}
|
||||
|
||||
config.lora.region = selectedRegion;
|
||||
auto changes = SEGMENT_CONFIG;
|
||||
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
||||
if (crypto) {
|
||||
crypto->ensurePkiKeys(config.security, owner);
|
||||
bool hamMode = getRegion(selectedRegion)->profile->licensedOnly;
|
||||
if (hamMode) {
|
||||
LOG_INFO("User chose an amateur radio mode region");
|
||||
pendingRegion = selectedRegion;
|
||||
menuQueue = HamModeConfirm;
|
||||
screen->runNow();
|
||||
} else if (owner.is_licensed) {
|
||||
LOG_INFO("Licensed user chose a non-ham region; prompting to revert licensed mode");
|
||||
pendingRegion = selectedRegion;
|
||||
menuQueue = LicensedToNormalConfirm;
|
||||
screen->runNow();
|
||||
} else {
|
||||
applyLoraRegion(selectedRegion, false);
|
||||
}
|
||||
#endif
|
||||
config.lora.tx_enabled = true;
|
||||
initRegion();
|
||||
if (myRegion->dutyCycle < 100) {
|
||||
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
|
||||
}
|
||||
|
||||
if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) {
|
||||
// Default broker is in use, so subscribe to the appropriate MQTT root topic for this region
|
||||
sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name);
|
||||
changes |= SEGMENT_MODULECONFIG;
|
||||
}
|
||||
|
||||
service->reloadConfig(changes);
|
||||
});
|
||||
|
||||
bannerOptions.durationMs = duration;
|
||||
@@ -271,6 +303,38 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::hamModeConfirmMenu()
|
||||
{
|
||||
static const char *confirmOptions[] = {"No", "Yes"};
|
||||
BannerOverlayOptions confirmBanner;
|
||||
confirmBanner.message = "I confirm I am a\nlicensed amateur\nradio operator";
|
||||
confirmBanner.optionsArrayPtr = confirmOptions;
|
||||
confirmBanner.optionsCount = 2;
|
||||
confirmBanner.bannerCallback = [](int selected) {
|
||||
if (selected == 1)
|
||||
applyLoraRegion(pendingRegion, true);
|
||||
};
|
||||
screen->showOverlayBanner(confirmBanner);
|
||||
}
|
||||
|
||||
void menuHandler::licensedToNormalConfirmMenu()
|
||||
{
|
||||
static const char *confirmOptions[] = {"Keep licensed", "Revert to Normal"};
|
||||
BannerOverlayOptions confirmBanner;
|
||||
confirmBanner.message = "Revert licensed\nmode? This will\nre-enable encryption.";
|
||||
confirmBanner.optionsArrayPtr = confirmOptions;
|
||||
confirmBanner.optionsCount = 2;
|
||||
confirmBanner.bannerCallback = [](int selected) {
|
||||
if (selected == 1) {
|
||||
owner.is_licensed = false;
|
||||
config.lora.override_duty_cycle = false;
|
||||
service->reloadOwner(false);
|
||||
}
|
||||
applyLoraRegion(pendingRegion, false);
|
||||
};
|
||||
screen->showOverlayBanner(confirmBanner);
|
||||
}
|
||||
|
||||
void menuHandler::deviceRolePicker()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
|
||||
@@ -378,42 +442,64 @@ void menuHandler::FrequencySlotPicker()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
// Maximum presets any region can have + 1 for Back
|
||||
static constexpr int MAX_PRESET_OPTIONS = 16;
|
||||
|
||||
static BannerOverlayOptions buildRegionPresetBanner()
|
||||
{
|
||||
// Static storage reused each call — safe because the banner is shown immediately after.
|
||||
static const char *optionsArray[MAX_PRESET_OPTIONS];
|
||||
static int optionsEnumArray[MAX_PRESET_OPTIONS];
|
||||
static char presetLabelBuf[MAX_PRESET_OPTIONS][12]; // scratch space for name copies
|
||||
int count = 0;
|
||||
|
||||
optionsArray[count] = "Back";
|
||||
optionsEnumArray[count++] = -1;
|
||||
|
||||
if (myRegion && myRegion->profile) {
|
||||
const meshtastic_Config_LoRaConfig_ModemPreset *presets = myRegion->getAvailablePresets();
|
||||
size_t numPresets = myRegion->getNumPresets();
|
||||
for (size_t i = 0; i < numPresets && count < MAX_PRESET_OPTIONS; ++i) {
|
||||
const char *name = DisplayFormatters::getModemPresetDisplayName(presets[i], false, true);
|
||||
strncpy(presetLabelBuf[count], name, sizeof(presetLabelBuf[count]) - 1);
|
||||
presetLabelBuf[count][sizeof(presetLabelBuf[count]) - 1] = '\0';
|
||||
optionsArray[count] = presetLabelBuf[count];
|
||||
optionsEnumArray[count++] = static_cast<int>(presets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
int initialSelection = 0;
|
||||
for (int i = 1; i < count; ++i) {
|
||||
if (optionsEnumArray[i] == static_cast<int>(config.lora.modem_preset)) {
|
||||
initialSelection = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Radio Preset";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.optionsCount = static_cast<uint8_t>(count);
|
||||
bannerOptions.InitialSelected = initialSelection;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == -1) {
|
||||
menuHandler::menuQueue = menuHandler::LoraMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
config.lora.use_preset = true;
|
||||
config.lora.modem_preset = static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(selected);
|
||||
config.lora.channel_num = 0; // Reset to default channel for the preset
|
||||
config.lora.override_frequency = 0; // Clear any custom frequency
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
};
|
||||
return bannerOptions;
|
||||
}
|
||||
|
||||
void menuHandler::radioPresetPicker()
|
||||
{
|
||||
static const RadioPresetOption presetOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{"LongTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO},
|
||||
{"LongModerate", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE},
|
||||
{"LongFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST},
|
||||
{"MediumSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW},
|
||||
{"MediumFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST},
|
||||
{"ShortSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW},
|
||||
{"ShortFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST},
|
||||
{"ShortTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO},
|
||||
};
|
||||
|
||||
constexpr size_t presetCount = sizeof(presetOptions) / sizeof(presetOptions[0]);
|
||||
static std::array<const char *, presetCount> presetLabels{};
|
||||
|
||||
auto bannerOptions =
|
||||
createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuHandler::menuQueue = menuHandler::LoraMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!option.hasValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
config.lora.modem_preset = option.value;
|
||||
config.lora.channel_num = 0; // Reset to default channel for the preset
|
||||
config.lora.override_frequency = 0; // Clear any custom frequency
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
});
|
||||
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
screen->showOverlayBanner(buildRegionPresetBanner());
|
||||
}
|
||||
|
||||
void menuHandler::twelveHourPicker()
|
||||
@@ -1307,6 +1393,11 @@ void menuHandler::positionBaseMenu()
|
||||
if (accelerometerThread) {
|
||||
accelerometerThread->calibrate(30);
|
||||
}
|
||||
#endif
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
|
||||
if (magnetometerThread) {
|
||||
magnetometerThread->calibrate(30);
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case PositionAction::GPSSmartPosition:
|
||||
@@ -2787,6 +2878,12 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case ThemeMenu:
|
||||
themeMenu();
|
||||
break;
|
||||
case HamModeConfirm:
|
||||
hamModeConfirmMenu();
|
||||
break;
|
||||
case LicensedToNormalConfirm:
|
||||
licensedToNormalConfirmMenu();
|
||||
break;
|
||||
}
|
||||
menuQueue = MenuNone;
|
||||
}
|
||||
|
||||
@@ -55,10 +55,13 @@ class menuHandler
|
||||
FrameToggles,
|
||||
DisplayUnits,
|
||||
MessageBubblesMenu,
|
||||
ThemeMenu
|
||||
ThemeMenu,
|
||||
HamModeConfirm,
|
||||
LicensedToNormalConfirm
|
||||
};
|
||||
static screenMenus menuQueue;
|
||||
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
|
||||
static meshtastic_Config_LoRaConfig_RegionCode pendingRegion;
|
||||
|
||||
static void OnboardMessage();
|
||||
static void LoraRegionPicker(uint32_t duration = 30000);
|
||||
@@ -111,6 +114,8 @@ class menuHandler
|
||||
static void messageBubblesMenu();
|
||||
static void themeMenu();
|
||||
static void textMessageMenu();
|
||||
static void hamModeConfirmMenu();
|
||||
static void licensedToNormalConfirmMenu();
|
||||
|
||||
private:
|
||||
static void saveUIConfig();
|
||||
|
||||
@@ -154,8 +154,8 @@ static std::vector<uint32_t> seenPeers;
|
||||
// Public helper so menus / store can clear stale registries
|
||||
void clearThreadRegistries()
|
||||
{
|
||||
seenChannels.clear();
|
||||
seenPeers.clear();
|
||||
std::vector<int>().swap(seenChannels);
|
||||
std::vector<uint32_t>().swap(seenPeers);
|
||||
}
|
||||
|
||||
// Setter so other code can switch threads
|
||||
@@ -387,6 +387,58 @@ static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int to
|
||||
}
|
||||
}
|
||||
|
||||
static void appendWrappedLines(OLEDDisplay *display, const char *messageBuf, int textWidth, std::vector<std::string> &allLines,
|
||||
size_t maxTotalLines, size_t maxWrappedLines)
|
||||
{
|
||||
std::string line;
|
||||
std::string word;
|
||||
size_t wrappedCount = 0;
|
||||
|
||||
auto appendLine = [&](std::string &&newLine) -> bool {
|
||||
if (newLine.empty())
|
||||
return true;
|
||||
if (allLines.size() >= maxTotalLines || wrappedCount >= maxWrappedLines)
|
||||
return false;
|
||||
allLines.emplace_back(std::move(newLine));
|
||||
++wrappedCount;
|
||||
return true;
|
||||
};
|
||||
|
||||
for (int i = 0; messageBuf[i]; ++i) {
|
||||
char ch = messageBuf[i];
|
||||
if ((unsigned char)messageBuf[i] == 0xE2 && (unsigned char)messageBuf[i + 1] == 0x80 &&
|
||||
(unsigned char)messageBuf[i + 2] == 0x99) {
|
||||
ch = '\''; // plain apostrophe
|
||||
i += 2; // skip over the extra UTF-8 bytes
|
||||
}
|
||||
if (ch == '\n') {
|
||||
if (!word.empty())
|
||||
line += word;
|
||||
if (!appendLine(std::move(line)))
|
||||
return;
|
||||
line.clear();
|
||||
word.clear();
|
||||
} else if (ch == ' ') {
|
||||
line += word + ' ';
|
||||
word.clear();
|
||||
} else {
|
||||
word += ch;
|
||||
std::string test = line + word;
|
||||
uint16_t strWidth = graphics::UIRenderer::measureStringWithEmotes(display, test.c_str());
|
||||
if (strWidth > textWidth) {
|
||||
if (!line.empty() && !appendLine(std::move(line)))
|
||||
return;
|
||||
line = word;
|
||||
word.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!word.empty())
|
||||
line += word;
|
||||
appendLine(std::move(line));
|
||||
}
|
||||
|
||||
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
// Ensure any boot-relative timestamps are upgraded if RTC is valid
|
||||
@@ -646,19 +698,15 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
const char *msgText = MessageStore::getText(m);
|
||||
|
||||
int wrapWidth = mine ? rightTextWidth : leftTextWidth;
|
||||
std::vector<std::string> wrapped = generateLines(display, "", msgText, wrapWidth);
|
||||
// Per-message wrap-line limit: even if wrapping produces many lines, cap them to prevent
|
||||
// a single long message from consuming most or all of the cache.
|
||||
constexpr size_t MAX_WRAPPED_LINES_PER_MSG = 20U;
|
||||
size_t wrappedCount = 0;
|
||||
for (auto &ln : wrapped) {
|
||||
if (allLines.size() >= MAX_CACHED_LINES || wrappedCount >= MAX_WRAPPED_LINES_PER_MSG)
|
||||
break; // Cache limit or per-message limit reached; stop adding lines from this message
|
||||
allLines.emplace_back(std::move(ln));
|
||||
const size_t previousLineCount = allLines.size();
|
||||
appendWrappedLines(display, msgText, wrapWidth, allLines, MAX_CACHED_LINES, MAX_WRAPPED_LINES_PER_MSG);
|
||||
for (size_t i = previousLineCount; i < allLines.size(); ++i) {
|
||||
isMine.push_back(mine);
|
||||
isHeader.push_back(false);
|
||||
ackForLine.push_back(AckStatus::NONE);
|
||||
++wrappedCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1008,20 +1056,23 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
|
||||
|
||||
std::vector<int> rowHeights;
|
||||
rowHeights.reserve(lines.size());
|
||||
std::vector<graphics::EmoteRenderer::LineMetrics> lineMetrics;
|
||||
lineMetrics.reserve(lines.size());
|
||||
|
||||
for (const auto &line : lines) {
|
||||
lineMetrics.push_back(graphics::EmoteRenderer::analyzeLine(nullptr, line, FONT_HEIGHT_SMALL, emotes, numEmotes));
|
||||
}
|
||||
auto currentMetrics = graphics::EmoteRenderer::LineMetrics{};
|
||||
auto nextMetrics = lines.empty()
|
||||
? graphics::EmoteRenderer::LineMetrics{}
|
||||
: graphics::EmoteRenderer::analyzeLine(nullptr, lines[0], FONT_HEIGHT_SMALL, emotes, numEmotes);
|
||||
|
||||
for (size_t idx = 0; idx < lines.size(); ++idx) {
|
||||
currentMetrics = nextMetrics;
|
||||
nextMetrics = (idx + 1 < lines.size())
|
||||
? graphics::EmoteRenderer::analyzeLine(nullptr, lines[idx + 1], FONT_HEIGHT_SMALL, emotes, numEmotes)
|
||||
: graphics::EmoteRenderer::LineMetrics{};
|
||||
|
||||
const int baseHeight = FONT_HEIGHT_SMALL;
|
||||
int lineHeight = baseHeight;
|
||||
|
||||
const int tallestEmote = lineMetrics[idx].tallestHeight;
|
||||
const bool hasEmote = lineMetrics[idx].hasEmote;
|
||||
const bool nextHasEmote = (idx + 1 < lines.size()) && lineMetrics[idx + 1].hasEmote;
|
||||
const int tallestEmote = currentMetrics.tallestHeight;
|
||||
const bool hasEmote = currentMetrics.hasEmote;
|
||||
const bool nextHasEmote = (idx + 1 < lines.size()) && nextMetrics.hasEmote;
|
||||
|
||||
if (isHeaderVec[idx]) {
|
||||
// Header line spacing
|
||||
|
||||
@@ -66,6 +66,15 @@ uint32_t pow_of_10(uint32_t n)
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint64_t pow_of_16(uint32_t n)
|
||||
{
|
||||
uint64_t ret = 1;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
ret *= 16ULL;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
char graphics::NotificationRenderer::alertBannerLines[MAX_LINES + 1][64] = {};
|
||||
uint8_t graphics::NotificationRenderer::alertBannerLineCount = 0;
|
||||
graphics::NotificationRenderer::BannerFont graphics::NotificationRenderer::alertBannerLineFonts[MAX_LINES + 1] = {};
|
||||
@@ -259,6 +268,9 @@ void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayU
|
||||
case notificationTypeEnum::number_picker:
|
||||
drawNumberPicker(display, state);
|
||||
break;
|
||||
case notificationTypeEnum::hex_picker:
|
||||
drawHexPicker(display, state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,6 +357,105 @@ void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiS
|
||||
drawNotificationBox(display, state, linePointers, totalLines, 0);
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawHexPicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
const char *lineStarts[MAX_LINES + 1] = {0};
|
||||
uint16_t lineCount = 0;
|
||||
|
||||
// Parse lines
|
||||
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
|
||||
lineStarts[lineCount] = alertBannerMessage;
|
||||
|
||||
// Find lines
|
||||
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
||||
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
||||
if (lineStarts[lineCount + 1][0] == '\n')
|
||||
lineStarts[lineCount + 1] += 1;
|
||||
lineCount++;
|
||||
}
|
||||
// modulo to extract
|
||||
uint8_t this_digit = (currentNumber % (pow_of_16(numDigits - curSelected))) / (pow_of_16(numDigits - curSelected - 1));
|
||||
// Handle input
|
||||
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS ||
|
||||
inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
if (this_digit == 15) {
|
||||
currentNumber -= 15 * (pow_of_16(numDigits - curSelected - 1));
|
||||
} else {
|
||||
currentNumber += (pow_of_16(numDigits - curSelected - 1));
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS ||
|
||||
inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||
if (this_digit == 0) {
|
||||
currentNumber += 15 * (pow_of_16(numDigits - curSelected - 1));
|
||||
} else {
|
||||
currentNumber -= (pow_of_16(numDigits - curSelected - 1));
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
|
||||
if (inEvent.kbchar > 47 && inEvent.kbchar < 58) { // have a digit
|
||||
currentNumber -= this_digit * (pow_of_16(numDigits - curSelected - 1));
|
||||
currentNumber += (inEvent.kbchar - 48) * (pow_of_16(numDigits - curSelected - 1));
|
||||
curSelected++;
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
||||
curSelected++;
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
||||
curSelected--;
|
||||
} else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) &&
|
||||
alertBannerUntil != 0) {
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
if (curSelected == static_cast<int8_t>(numDigits)) {
|
||||
alertBannerCallback(currentNumber);
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||
if (alertBannerMessage[0] == '\0')
|
||||
return;
|
||||
|
||||
uint16_t totalLines = lineCount + 2;
|
||||
const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation
|
||||
|
||||
// copy the linestarts to display to the linePointers holder
|
||||
for (uint16_t i = 0; i < lineCount; i++) {
|
||||
linePointers[i] = lineStarts[i];
|
||||
}
|
||||
std::string digits = " ";
|
||||
std::string arrowPointer = " ";
|
||||
for (uint16_t i = 0; i < numDigits; i++) {
|
||||
// Modulo minus modulo to return just the current number
|
||||
uint8_t digitValue = (currentNumber % (pow_of_16(numDigits - i))) / (pow_of_16(numDigits - i - 1));
|
||||
if (digitValue < 10) {
|
||||
digits += std::to_string(digitValue) + " ";
|
||||
} else if (digitValue == 10) {
|
||||
digits += "A ";
|
||||
} else if (digitValue == 11) {
|
||||
digits += "B ";
|
||||
} else if (digitValue == 12) {
|
||||
digits += "C ";
|
||||
} else if (digitValue == 13) {
|
||||
digits += "D ";
|
||||
} else if (digitValue == 14) {
|
||||
digits += "E ";
|
||||
} else if (digitValue == 15) {
|
||||
digits += "F ";
|
||||
}
|
||||
|
||||
if (curSelected == i) {
|
||||
arrowPointer += "^ ";
|
||||
} else {
|
||||
arrowPointer += "_ ";
|
||||
}
|
||||
}
|
||||
|
||||
linePointers[lineCount++] = digits.c_str();
|
||||
linePointers[lineCount++] = arrowPointer.c_str();
|
||||
|
||||
drawNotificationBox(display, state, linePointers, totalLines, 0);
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
static uint32_t selectedNodenum = 0;
|
||||
|
||||
@@ -42,6 +42,7 @@ class NotificationRenderer
|
||||
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawHexPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#if HAS_SCREEN
|
||||
#include "CompassRenderer.h"
|
||||
#include "GPSStatus.h"
|
||||
#include "MeshRadio.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NodeListRenderer.h"
|
||||
@@ -78,10 +79,12 @@ static inline void transformNeedlePoint(float localX, float localY, float sinHea
|
||||
outY = static_cast<int16_t>(y);
|
||||
}
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
static float getCompassRingAngleOffset(float heading)
|
||||
{
|
||||
return (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) ? -heading : 0.0f;
|
||||
}
|
||||
#endif
|
||||
|
||||
static inline StandardCompassNeedlePoints computeStandardCompassNeedlePoints(int16_t compassX, int16_t compassY,
|
||||
uint16_t compassDiam, float headingRadian,
|
||||
@@ -816,16 +819,16 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
||||
// Helper to get SNR limit based on modem preset
|
||||
auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float {
|
||||
switch (preset) {
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST:
|
||||
case PRESET(LONG_SLOW):
|
||||
case PRESET(LONG_MODERATE):
|
||||
case PRESET(LONG_FAST):
|
||||
return -6.0f;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
|
||||
case PRESET(MEDIUM_SLOW):
|
||||
case PRESET(MEDIUM_FAST):
|
||||
return -5.5f;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
|
||||
case PRESET(SHORT_SLOW):
|
||||
case PRESET(SHORT_FAST):
|
||||
case PRESET(SHORT_TURBO):
|
||||
return -4.5f;
|
||||
default:
|
||||
return -6.0f;
|
||||
@@ -1141,11 +1144,16 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
bool origBold = config.display.heading_bold;
|
||||
config.display.heading_bold = false;
|
||||
|
||||
// Display Region and Channel Utilization
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
|
||||
if (!config.lora.tx_enabled) {
|
||||
const char *txdisabled = "Transmit Disabled";
|
||||
display->drawString(x, getTextPositions(display)[line], txdisabled);
|
||||
} else {
|
||||
drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
|
||||
// Display Region and Channel Utilization
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
|
||||
} else {
|
||||
drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
|
||||
}
|
||||
}
|
||||
char uptimeStr[32] = "";
|
||||
if (currentResolution != ScreenResolution::UltraLow) {
|
||||
|
||||
@@ -6,15 +6,12 @@
|
||||
FastEPD buffer format: 1bpp, horizontal bytes, MSB = leftmost pixel, 1 = white
|
||||
|
||||
Both formats share the same pixel layout and polarity (1 = white, 0 = black).
|
||||
The InkHUD safe-area buffer (944×523) is copied into the centre of the physical
|
||||
The InkHUD safe-area buffer (928×508) is copied into the centre of the physical
|
||||
960×540 FastEPD buffer so content clears the panel's inactive edge border.
|
||||
See ED047TC1.h for the H_OFFSET_BYTES / V_OFFSET_TOP / V_OFFSET_BOTTOM constants.
|
||||
|
||||
*/
|
||||
|
||||
// Ruler diagnostic — uncomment to draw calibration lines at each physical edge.
|
||||
// #define EINK_EDGE_LINES
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#ifdef T5_S3_EPAPER_PRO
|
||||
|
||||
@@ -212,35 +209,6 @@ void ED047TC1::update(uint8_t *imageData, UpdateTypes type)
|
||||
memcpy(dstRow, srcRow, srcRowBytes);
|
||||
}
|
||||
|
||||
#ifdef EINK_EDGE_LINES
|
||||
// Draw a 1px black box at the exact boundary of the safe area within the
|
||||
// physical buffer. If the margins are correct, all 4 lines should be
|
||||
// fully visible and right at the edge of the usable display area.
|
||||
|
||||
auto setPixelBlack = [&](uint32_t col, uint32_t row) { cur[row * dstRowBytes + col / 8] &= ~(0x80 >> (col % 8)); };
|
||||
|
||||
const uint32_t safeX = H_OFFSET_BYTES * 8;
|
||||
const uint32_t safeY = V_OFFSET_TOP;
|
||||
const uint32_t safeW = DISPLAY_WIDTH;
|
||||
const uint32_t safeH = DISPLAY_HEIGHT;
|
||||
|
||||
// Top edge: horizontal line at safeY
|
||||
for (uint32_t col = safeX; col < safeX + safeW; col++)
|
||||
setPixelBlack(col, safeY);
|
||||
|
||||
// Bottom edge: horizontal line at safeY + safeH - 1
|
||||
for (uint32_t col = safeX; col < safeX + safeW; col++)
|
||||
setPixelBlack(col, safeY + safeH - 1);
|
||||
|
||||
// Left edge: vertical line at safeX
|
||||
for (uint32_t row = safeY; row < safeY + safeH; row++)
|
||||
setPixelBlack(safeX, row);
|
||||
|
||||
// Right edge: vertical line at safeX + safeW - 1
|
||||
for (uint32_t row = safeY; row < safeY + safeH; row++)
|
||||
setPixelBlack(safeX + safeW - 1, row);
|
||||
#endif
|
||||
|
||||
if (type == FULL) {
|
||||
epaper->fullUpdate(CLEAR_SLOW, false);
|
||||
epaper->backupPlane(); // Sync pPrevious so next partialUpdate has a correct baseline
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
V_OFFSET_TOP and V_OFFSET_BOTTOM (vertical, pixel rows) to position it.
|
||||
|
||||
Changing these constants shifts content inward from each physical edge:
|
||||
H_OFFSET_BYTES = 1 → 8px left margin, 8px right margin (960 – 8 – 8 = 944)
|
||||
V_OFFSET_TOP = 9 → 9px top margin (asymmetric: top ≠ bottom)
|
||||
V_OFFSET_BOTTOM = 8 → 8px bottom margin (540 – 9 – 8 = 523)
|
||||
H_OFFSET_BYTES = 2 → 16px left margin, 16px right margin (960 – 16 – 16 = 928)
|
||||
V_OFFSET_TOP = 16 → 16px top margin
|
||||
V_OFFSET_BOTTOM = 16 → 16px bottom margin (540 – 16 – 16 = 508)
|
||||
|
||||
*/
|
||||
|
||||
@@ -61,13 +61,13 @@ class ED047TC1 : public EInk
|
||||
//
|
||||
// Calibrated by flashing a 1px border box and adjusting until all 4 sides are visible.
|
||||
|
||||
static constexpr uint16_t DISPLAY_WIDTH = 944; // 960 − H_OFFSET_BYTES×8 − right_margin (8+8 = 16px)
|
||||
static constexpr uint16_t DISPLAY_HEIGHT = 523; // 540 − V_OFFSET_TOP − V_OFFSET_BOTTOM (9+8 = 17px)
|
||||
static constexpr uint16_t DISPLAY_WIDTH = 928; // 960 − H_OFFSET_BYTES×8 − right_margin (16+16 = 32px)
|
||||
static constexpr uint16_t DISPLAY_HEIGHT = 508; // 540 − V_OFFSET_TOP − V_OFFSET_BOTTOM (16+16 = 32px)
|
||||
|
||||
static constexpr uint8_t H_OFFSET_BYTES = 1; // visual TOP : 8px physical left margin
|
||||
// visual BOTTOM: 960−8−944=8px physical right margin
|
||||
static constexpr uint8_t V_OFFSET_TOP = 9; // visual RIGHT : CONFIRMED OK
|
||||
static constexpr uint8_t V_OFFSET_BOTTOM = 8; // visual LEFT : 8px physical bottom margin
|
||||
static constexpr uint8_t H_OFFSET_BYTES = 2; // visual TOP : 16px physical left margin
|
||||
// visual BOTTOM: 960−16−928=16px physical right margin
|
||||
static constexpr uint8_t V_OFFSET_TOP = 16; // visual RIGHT : 16px physical top margin
|
||||
static constexpr uint8_t V_OFFSET_BOTTOM = 16; // visual LEFT : 16px physical bottom margin
|
||||
|
||||
static constexpr UpdateTypes supported = static_cast<UpdateTypes>(FULL | FAST);
|
||||
|
||||
|
||||
@@ -64,6 +64,12 @@ enum MenuAction {
|
||||
SET_REGION_KZ_863,
|
||||
SET_REGION_NP_865,
|
||||
SET_REGION_BR_902,
|
||||
SET_REGION_EU_866,
|
||||
SET_REGION_NARROW_868,
|
||||
SET_REGION_ITU1_2M,
|
||||
SET_REGION_ITU2_2M,
|
||||
SET_REGION_ITU3_2M,
|
||||
SET_REGION_ITU2_125CM,
|
||||
// Device Roles
|
||||
SET_ROLE_CLIENT,
|
||||
SET_ROLE_CLIENT_MUTE,
|
||||
@@ -78,6 +84,13 @@ enum MenuAction {
|
||||
SET_PRESET_SHORT_SLOW,
|
||||
SET_PRESET_SHORT_FAST,
|
||||
SET_PRESET_SHORT_TURBO,
|
||||
SET_PRESET_LITE_SLOW,
|
||||
SET_PRESET_LITE_FAST,
|
||||
SET_PRESET_NARROW_SLOW,
|
||||
SET_PRESET_NARROW_FAST,
|
||||
SET_PRESET_TINY_SLOW,
|
||||
SET_PRESET_TINY_FAST,
|
||||
SET_PRESET_FROM_REGION, // Dynamic: preset chosen from region-available list
|
||||
// Timezones
|
||||
SET_TZ_US_HAWAII,
|
||||
SET_TZ_US_ALASKA,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "DisplayFormatters.h"
|
||||
#include "GPS.h"
|
||||
#include "MeshRadio.h"
|
||||
#include "MeshService.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
@@ -257,6 +258,11 @@ int32_t InkHUD::MenuApplet::runOnce()
|
||||
return OSThread::disable();
|
||||
}
|
||||
|
||||
// Storage for the dynamically-built region preset list — populated in showPage(NODE_CONFIG_PRESET)
|
||||
static constexpr uint8_t MAX_REGION_PRESETS = 16;
|
||||
static meshtastic_Config_LoRaConfig_ModemPreset regionPresets[MAX_REGION_PRESETS];
|
||||
static uint8_t regionPresetCount = 0;
|
||||
|
||||
static void applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode region)
|
||||
{
|
||||
if (config.lora.region == region)
|
||||
@@ -276,7 +282,7 @@ static void applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode region)
|
||||
|
||||
initRegion();
|
||||
|
||||
if (myRegion && myRegion->dutyCycle < 100) {
|
||||
if (myRegion && getEffectiveDutyCycle() < 100) {
|
||||
config.lora.ignore_mqtt = true;
|
||||
}
|
||||
|
||||
@@ -770,6 +776,30 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_BR_902);
|
||||
break;
|
||||
|
||||
case SET_REGION_EU_866:
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_866);
|
||||
break;
|
||||
|
||||
case SET_REGION_NARROW_868:
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868);
|
||||
break;
|
||||
|
||||
case SET_REGION_ITU1_2M:
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M);
|
||||
break;
|
||||
|
||||
case SET_REGION_ITU2_2M:
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ITU2_2M);
|
||||
break;
|
||||
|
||||
case SET_REGION_ITU3_2M:
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M);
|
||||
break;
|
||||
|
||||
case SET_REGION_ITU2_125CM:
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM);
|
||||
break;
|
||||
|
||||
// Roles
|
||||
case SET_ROLE_CLIENT:
|
||||
applyDeviceRole(meshtastic_Config_DeviceConfig_Role_CLIENT);
|
||||
@@ -789,37 +819,62 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
|
||||
// Presets
|
||||
case SET_PRESET_LONG_SLOW:
|
||||
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW);
|
||||
applyLoRaPreset(PRESET(LONG_SLOW));
|
||||
break;
|
||||
|
||||
case SET_PRESET_LONG_MODERATE:
|
||||
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE);
|
||||
applyLoRaPreset(PRESET(LONG_MODERATE));
|
||||
break;
|
||||
|
||||
case SET_PRESET_LONG_FAST:
|
||||
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
|
||||
applyLoRaPreset(PRESET(LONG_FAST));
|
||||
break;
|
||||
|
||||
case SET_PRESET_MEDIUM_SLOW:
|
||||
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW);
|
||||
applyLoRaPreset(PRESET(MEDIUM_SLOW));
|
||||
break;
|
||||
|
||||
case SET_PRESET_MEDIUM_FAST:
|
||||
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST);
|
||||
applyLoRaPreset(PRESET(MEDIUM_FAST));
|
||||
break;
|
||||
|
||||
case SET_PRESET_SHORT_SLOW:
|
||||
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW);
|
||||
applyLoRaPreset(PRESET(SHORT_SLOW));
|
||||
break;
|
||||
|
||||
case SET_PRESET_SHORT_FAST:
|
||||
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST);
|
||||
applyLoRaPreset(PRESET(SHORT_FAST));
|
||||
break;
|
||||
|
||||
case SET_PRESET_SHORT_TURBO:
|
||||
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO);
|
||||
applyLoRaPreset(PRESET(SHORT_TURBO));
|
||||
break;
|
||||
|
||||
case SET_PRESET_NARROW_SLOW:
|
||||
applyLoRaPreset(PRESET(NARROW_SLOW));
|
||||
break;
|
||||
|
||||
case SET_PRESET_NARROW_FAST:
|
||||
applyLoRaPreset(PRESET(NARROW_FAST));
|
||||
break;
|
||||
|
||||
case SET_PRESET_TINY_SLOW:
|
||||
applyLoRaPreset(PRESET(TINY_SLOW));
|
||||
break;
|
||||
|
||||
case SET_PRESET_TINY_FAST:
|
||||
applyLoRaPreset(PRESET(TINY_FAST));
|
||||
break;
|
||||
|
||||
case SET_PRESET_FROM_REGION: {
|
||||
// cursor - 1 because index 0 is "Back"
|
||||
const uint8_t index = cursor - 1;
|
||||
if (index < regionPresetCount) {
|
||||
applyLoRaPreset(regionPresets[index]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Timezones
|
||||
case SET_TZ_US_HAWAII:
|
||||
applyTimezone("HST10");
|
||||
@@ -1421,6 +1476,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
items.push_back(MenuItem("US", MenuAction::SET_REGION_US, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("EU 868", MenuAction::SET_REGION_EU_868, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("EU 433", MenuAction::SET_REGION_EU_433, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("EU 866", MenuAction::SET_REGION_EU_866, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("EU 868 Narrow", MenuAction::SET_REGION_NARROW_868, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("CN", MenuAction::SET_REGION_CN, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("JP", MenuAction::SET_REGION_JP, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("ANZ", MenuAction::SET_REGION_ANZ, MenuPage::EXIT));
|
||||
@@ -1444,19 +1501,27 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
items.push_back(MenuItem("KZ 863", MenuAction::SET_REGION_KZ_863, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("NP 865", MenuAction::SET_REGION_NP_865, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("BR 902", MenuAction::SET_REGION_BR_902, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("ITU1_2M (144-146)", MenuAction::SET_REGION_ITU1_2M, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("ITU2_2M (144-148)", MenuAction::SET_REGION_ITU2_2M, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("ITU3_2M (144-148)", MenuAction::SET_REGION_ITU3_2M, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("ITU2_125CM (220-225)", MenuAction::SET_REGION_ITU2_125CM, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
break;
|
||||
|
||||
case NODE_CONFIG_PRESET: {
|
||||
previousPage = MenuPage::NODE_CONFIG_LORA;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
items.push_back(MenuItem("Long Moderate", MenuAction::SET_PRESET_LONG_MODERATE, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Long Fast", MenuAction::SET_PRESET_LONG_FAST, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Medium Slow", MenuAction::SET_PRESET_MEDIUM_SLOW, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Medium Fast", MenuAction::SET_PRESET_MEDIUM_FAST, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Short Slow", MenuAction::SET_PRESET_SHORT_SLOW, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Short Fast", MenuAction::SET_PRESET_SHORT_FAST, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Short Turbo", MenuAction::SET_PRESET_SHORT_TURBO, MenuPage::EXIT));
|
||||
regionPresetCount = 0;
|
||||
if (myRegion && myRegion->profile) {
|
||||
const meshtastic_Config_LoRaConfig_ModemPreset *presets = myRegion->getAvailablePresets();
|
||||
size_t numPresets = myRegion->getNumPresets();
|
||||
for (size_t i = 0; i < numPresets && regionPresetCount < MAX_REGION_PRESETS; ++i) {
|
||||
regionPresets[regionPresetCount++] = presets[i];
|
||||
const char *name = DisplayFormatters::getModemPresetDisplayName(presets[i], false, true);
|
||||
nodeConfigLabels.emplace_back(name);
|
||||
items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::SET_PRESET_FROM_REGION, MenuPage::EXIT));
|
||||
}
|
||||
}
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "./NotificationApplet.h"
|
||||
|
||||
#include "./Notification.h"
|
||||
#include "MessageStore.h"
|
||||
#include "graphics/niche/InkHUD/Persistence.h"
|
||||
|
||||
#include "meshUtils.h"
|
||||
@@ -231,7 +232,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
|
||||
bool msgIsBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
|
||||
|
||||
// Pick source of message
|
||||
const MessageStore::Message *message =
|
||||
const StoredMessage *message =
|
||||
msgIsBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
|
||||
|
||||
// Find info about the sender
|
||||
@@ -261,7 +262,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
|
||||
text += hexifyNodeNum(message->sender);
|
||||
|
||||
text += ": ";
|
||||
text += message->text;
|
||||
text += MessageStore::getText(*message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "./AllMessageApplet.h"
|
||||
|
||||
#include "MessageStore.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::AllMessageApplet::onActivate()
|
||||
@@ -37,7 +39,7 @@ int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *
|
||||
void InkHUD::AllMessageApplet::onRender(bool full)
|
||||
{
|
||||
// Find newest message, regardless of whether DM or broadcast
|
||||
MessageStore::Message *message;
|
||||
StoredMessage *message;
|
||||
if (latestMessage->wasBroadcast)
|
||||
message = &latestMessage->broadcast;
|
||||
else
|
||||
@@ -96,7 +98,7 @@ void InkHUD::AllMessageApplet::onRender(bool full)
|
||||
// ===================
|
||||
|
||||
// Parse any non-ascii chars in the message
|
||||
std::string text = parse(message->text);
|
||||
std::string text = parse(std::string(MessageStore::getText(*message)));
|
||||
|
||||
// Extra gap below the header
|
||||
int16_t textTop = headerDivY + padDivH;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "./DMApplet.h"
|
||||
|
||||
#include "MessageStore.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::DMApplet::onActivate()
|
||||
@@ -92,7 +94,7 @@ void InkHUD::DMApplet::onRender(bool full)
|
||||
// ===================
|
||||
|
||||
// Parse any non-ascii chars in the message
|
||||
std::string text = parse(latestMessage->dm.text);
|
||||
std::string text = parse(std::string(MessageStore::getText(latestMessage->dm)));
|
||||
|
||||
// Extra gap below the header
|
||||
int16_t textTop = headerDivY + padDivH;
|
||||
|
||||
@@ -7,19 +7,9 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
// Hard limits on how much message data to write to flash
|
||||
// Avoid filling the storage if something goes wrong
|
||||
// Normal usage should be well below this size
|
||||
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
|
||||
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
|
||||
|
||||
InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex)
|
||||
: SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex)
|
||||
{
|
||||
// Create the message store
|
||||
// Will shortly attempt to load messages from RAM, if applet is active
|
||||
// Label (filename in flash) is set from channel index
|
||||
store = new MessageStore("ch" + to_string(channelIndex));
|
||||
}
|
||||
|
||||
void InkHUD::ThreadedMessageApplet::onRender(bool full)
|
||||
@@ -61,17 +51,24 @@ void InkHUD::ThreadedMessageApplet::onRender(bool full)
|
||||
const uint16_t msgW = (msgR - msgL) + 1;
|
||||
|
||||
int16_t msgB = height() - 1; // Vertical cursor for drawing. Messages are bottom-aligned to this value.
|
||||
uint8_t i = 0; // Index of stored message
|
||||
|
||||
// Loop over messages
|
||||
// - until no messages left, or
|
||||
// - until no part of message fits on screen
|
||||
while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) {
|
||||
// Iterate the global store newest-first, showing only broadcast messages on our channel
|
||||
const auto &allMessages = messageStore.getLiveMessages();
|
||||
int msgIdx = (int)allMessages.size() - 1;
|
||||
|
||||
while (msgB >= (0 - fontSmall.lineHeight()) && msgIdx >= 0) {
|
||||
|
||||
const StoredMessage &m = allMessages.at(msgIdx);
|
||||
|
||||
// Skip messages that don't belong to this channel or are DMs
|
||||
if (m.type != MessageType::BROADCAST || m.channelIndex != channelIndex) {
|
||||
msgIdx--;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Grab data for message
|
||||
const MessageStore::Message &m = store->messages.at(i);
|
||||
bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message
|
||||
std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message
|
||||
bool outgoing = (m.sender == myNodeInfo.my_node_num);
|
||||
std::string bodyText = parse(std::string(MessageStore::getText(m))); // Parse any non-ascii chars
|
||||
|
||||
// Cache bottom Y of message text
|
||||
// - Used when drawing vertical line alongside
|
||||
@@ -152,18 +149,13 @@ void InkHUD::ThreadedMessageApplet::onRender(bool full)
|
||||
// Move cursor up: padding before next message
|
||||
msgB -= fontSmall.lineHeight() * 0.5;
|
||||
|
||||
i++;
|
||||
msgIdx--;
|
||||
} // End of loop: drawing each message
|
||||
|
||||
// Fade effect:
|
||||
// Area immediately below the divider. Overdraw with sparse white lines.
|
||||
// Make text appear to pass behind the header
|
||||
hatchRegion(0, dividerY + 1, width(), fontSmall.lineHeight() / 3, 2, WHITE);
|
||||
|
||||
// If we've run out of screen to draw messages, we can drop any leftover data from the queue
|
||||
// Those messages have been pushed off the screen-top by newer ones
|
||||
while (i < store->messages.size())
|
||||
store->messages.pop_back();
|
||||
}
|
||||
|
||||
// Code which runs when the applet begins running
|
||||
@@ -198,16 +190,8 @@ ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_Me
|
||||
if (mp.to != NODENUM_BROADCAST)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Extract info into our slimmed-down "StoredMessage" type
|
||||
MessageStore::Message newMessage;
|
||||
newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
||||
newMessage.sender = mp.from;
|
||||
newMessage.channelIndex = mp.channel;
|
||||
newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size);
|
||||
|
||||
// Store newest message at front
|
||||
// These records are used when rendering, and also stored in flash at shutdown
|
||||
store->messages.push_front(newMessage);
|
||||
// Store in the global messageStore — this handles sender, timestamp, channel, text, and ack status
|
||||
messageStore.addFromPacket(mp);
|
||||
|
||||
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
|
||||
if (getFrom(&mp) != nodeDB->getNodeNum())
|
||||
@@ -232,37 +216,25 @@ bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Save several recent messages to flash
|
||||
// Stores the contents of ThreadedMessageApplet::messages
|
||||
// Just enough messages to fill the display
|
||||
// Messages are packed "back-to-back", to minimize blocks of flash used
|
||||
// Save messages to flash via the global messageStore.
|
||||
// The global store holds messages for all channels; no per-channel file is needed.
|
||||
void InkHUD::ThreadedMessageApplet::saveMessagesToFlash()
|
||||
{
|
||||
// Create a label (will become the filename in flash)
|
||||
std::string label = "ch" + to_string(channelIndex);
|
||||
|
||||
store->saveToFlash();
|
||||
messageStore.saveToFlash();
|
||||
}
|
||||
|
||||
// Load recent messages to flash
|
||||
// Fills ThreadedMessageApplet::messages with previous messages
|
||||
// Just enough messages have been stored to cover the display
|
||||
// Messages are loaded once by InkHUD::begin() before applets start.
|
||||
// Nothing to do here at per-applet activation time.
|
||||
void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash()
|
||||
{
|
||||
// Create a label (will become the filename in flash)
|
||||
std::string label = "ch" + to_string(channelIndex);
|
||||
|
||||
store->loadFromFlash();
|
||||
// No-op: messageStore.loadFromFlash() is called in InkHUD::begin()
|
||||
}
|
||||
|
||||
// Code to run when device is shutting down
|
||||
// This is in addition to any onDeactivate() code, which will also run
|
||||
// Todo: implement before a reboot also
|
||||
void InkHUD::ThreadedMessageApplet::onShutdown()
|
||||
{
|
||||
// Save our current set of messages to flash, provided the applet isn't disabled
|
||||
if (isActive())
|
||||
saveMessagesToFlash();
|
||||
// messageStore.saveToFlash() is called centrally by Events::beforeDeepSleep / beforeReboot
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -20,8 +20,8 @@ Suggest a max of two channel, to minimize fs usage?
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "MessageStore.h"
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
#include "graphics/niche/InkHUD/MessageStore.h"
|
||||
|
||||
#include "modules/TextMessageModule.h"
|
||||
|
||||
@@ -49,7 +49,6 @@ class ThreadedMessageApplet : public Applet, public SinglePortModule
|
||||
void saveMessagesToFlash();
|
||||
void loadMessagesFromFlash();
|
||||
|
||||
MessageStore *store; // Messages, held in RAM for use, ready to save to flash on shutdown
|
||||
uint8_t channelIndex = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "./Events.h"
|
||||
|
||||
#include "MessageStore.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "buzz.h"
|
||||
@@ -514,6 +515,7 @@ int InkHUD::Events::beforeReboot(void *unused)
|
||||
inkhud->persistence->saveLatestMessage();
|
||||
} else {
|
||||
NicheGraphics::clearFlashData();
|
||||
messageStore.clearAllMessages(); // also wipe the shared message store
|
||||
}
|
||||
|
||||
// Note: no forceUpdate call here
|
||||
@@ -532,32 +534,27 @@ int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet)
|
||||
if (getFrom(packet) == nodeDB->getNodeNum())
|
||||
return 0;
|
||||
|
||||
// Determine whether the message is broadcast or a DM
|
||||
// Store this info to prevent confusion after a reboot
|
||||
// Avoids need to compare timestamps, because of situation where "future" messages block newly received, if time not set
|
||||
inkhud->persistence->latestMessage.wasBroadcast = isBroadcast(packet->to);
|
||||
bool isBroadcastMsg = isBroadcast(packet->to);
|
||||
inkhud->persistence->latestMessage.wasBroadcast = isBroadcastMsg;
|
||||
|
||||
// Pick the appropriate variable to store the message in
|
||||
MessageStore::Message *storedMessage = inkhud->persistence->latestMessage.wasBroadcast
|
||||
? &inkhud->persistence->latestMessage.broadcast
|
||||
: &inkhud->persistence->latestMessage.dm;
|
||||
|
||||
// Store nodenum of the sender
|
||||
// Applets can use this to fetch user data from nodedb, if they want
|
||||
storedMessage->sender = packet->from;
|
||||
|
||||
// Store the time (epoch seconds) when message received
|
||||
storedMessage->timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
||||
|
||||
// Store the channel
|
||||
// - (potentially) used to determine whether notification shows
|
||||
// - (potentially) used to determine which applet to focus
|
||||
storedMessage->channelIndex = packet->channel;
|
||||
|
||||
// Store the text
|
||||
// Need to specify manually how many bytes, because source not null-terminated
|
||||
storedMessage->text =
|
||||
std::string(&packet->decoded.payload.bytes[0], &packet->decoded.payload.bytes[packet->decoded.payload.size]);
|
||||
if (!isBroadcastMsg) {
|
||||
// DMs never pass through ThreadedMessageApplet, so add them to the global store here
|
||||
// so they survive reboots. Derive the latestMessage cache entry from the stored result.
|
||||
inkhud->persistence->latestMessage.dm = messageStore.addFromPacket(*packet);
|
||||
} else {
|
||||
// Broadcasts are added to the global store by ThreadedMessageApplet::handleReceived().
|
||||
// Here we only update the latestMessage cache used by AllMessageApplet / NotificationApplet.
|
||||
StoredMessage &sm = inkhud->persistence->latestMessage.broadcast;
|
||||
sm.sender = packet->from;
|
||||
sm.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
sm.channelIndex = packet->channel;
|
||||
const char *payload = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
|
||||
size_t storedLen = packet->decoded.payload.size;
|
||||
if (storedLen >= MAX_MESSAGE_SIZE)
|
||||
storedLen = MAX_MESSAGE_SIZE - 1;
|
||||
sm.textOffset = MessageStore::storeText(payload, storedLen);
|
||||
sm.textLength = static_cast<uint16_t>(storedLen);
|
||||
}
|
||||
|
||||
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
#include "./SystemApplet.h"
|
||||
#include "./Tile.h"
|
||||
#include "./WindowManager.h"
|
||||
#include "FSCommon.h"
|
||||
#include "MessageStore.h"
|
||||
#include "SPILock.h"
|
||||
#include "concurrency/LockGuard.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
@@ -60,11 +64,102 @@ void InkHUD::InkHUD::notifyApplyingChanges()
|
||||
}
|
||||
}
|
||||
|
||||
// One-time migration from the old per-channel InkHUD message files (/NicheGraphics/ch*.msgs)
|
||||
// to the firmware-wide MessageStore format (/Messages_default.msgs).
|
||||
// Only runs when the new store loaded empty, meaning this is the first boot after the format change.
|
||||
// Old files are deleted once migrated.
|
||||
static void migrateOldInkHUDMessages()
|
||||
{
|
||||
#ifdef FSCom
|
||||
bool migrated = false;
|
||||
constexpr uint8_t MAX_CHANNELS = 8;
|
||||
constexpr uint32_t OLD_MAX_MSG_SIZE = 250;
|
||||
|
||||
for (uint8_t ch = 0; ch < MAX_CHANNELS; ch++) {
|
||||
std::string path = "/NicheGraphics/ch";
|
||||
path += std::to_string(ch);
|
||||
path += ".msgs";
|
||||
|
||||
spiLock->lock();
|
||||
bool exists = FSCom.exists(path.c_str());
|
||||
spiLock->unlock();
|
||||
if (!exists)
|
||||
continue;
|
||||
|
||||
concurrency::LockGuard guard(spiLock);
|
||||
|
||||
auto f = FSCom.open(path.c_str(), FILE_O_READ);
|
||||
if (!f || f.size() == 0) {
|
||||
if (f)
|
||||
f.close();
|
||||
FSCom.remove(path.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t count = 0;
|
||||
f.readBytes(reinterpret_cast<char *>(&count), 1);
|
||||
|
||||
std::vector<StoredMessage> channelMsgs;
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
StoredMessage sm;
|
||||
f.readBytes(reinterpret_cast<char *>(&sm.timestamp), sizeof(sm.timestamp));
|
||||
f.readBytes(reinterpret_cast<char *>(&sm.sender), sizeof(sm.sender));
|
||||
f.readBytes(reinterpret_cast<char *>(&sm.channelIndex), sizeof(sm.channelIndex));
|
||||
|
||||
char textBuf[OLD_MAX_MSG_SIZE + 1] = {};
|
||||
uint32_t textLen = 0;
|
||||
char c;
|
||||
while (textLen < OLD_MAX_MSG_SIZE) {
|
||||
if (f.readBytes(&c, 1) != 1)
|
||||
break;
|
||||
if (c == '\0')
|
||||
break;
|
||||
textBuf[textLen++] = c;
|
||||
}
|
||||
|
||||
sm.dest = NODENUM_BROADCAST;
|
||||
sm.type = MessageType::BROADCAST;
|
||||
sm.isBootRelative = false;
|
||||
sm.ackStatus = AckStatus::ACKED;
|
||||
size_t storedLen = (textLen >= MAX_MESSAGE_SIZE) ? MAX_MESSAGE_SIZE - 1 : textLen;
|
||||
sm.textOffset = MessageStore::storeText(textBuf, storedLen);
|
||||
sm.textLength = static_cast<uint16_t>(storedLen);
|
||||
|
||||
channelMsgs.push_back(sm);
|
||||
}
|
||||
|
||||
// Old format stored newest-first (push_front); insert oldest-first for correct chronological order
|
||||
for (int i = static_cast<int>(channelMsgs.size()) - 1; i >= 0; i--)
|
||||
messageStore.addLiveMessage(channelMsgs[i]);
|
||||
if (!channelMsgs.empty())
|
||||
migrated = true;
|
||||
|
||||
f.close();
|
||||
FSCom.remove(path.c_str());
|
||||
LOG_INFO("Migrated %u messages from %s", static_cast<uint32_t>(count), path.c_str());
|
||||
}
|
||||
|
||||
// Delete the old latest.msgs; the latestMessage cache will be re-derived from migrated channel messages
|
||||
spiLock->lock();
|
||||
if (FSCom.exists("/NicheGraphics/latest.msgs"))
|
||||
FSCom.remove("/NicheGraphics/latest.msgs");
|
||||
spiLock->unlock();
|
||||
|
||||
if (migrated) {
|
||||
LOG_INFO("InkHUD message migration complete, saving to new format");
|
||||
messageStore.saveToFlash();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Start InkHUD!
|
||||
// Call this only after you have configured InkHUD
|
||||
void InkHUD::InkHUD::begin()
|
||||
{
|
||||
persistence->loadSettings();
|
||||
messageStore.loadFromFlash(); // Load persisted messages before deriving latestMessage cache
|
||||
if (messageStore.getLiveMessages().empty())
|
||||
migrateOldInkHUDMessages(); // First boot after format change: import old per-channel files
|
||||
persistence->loadLatestMessage();
|
||||
|
||||
windowManager->begin();
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./MessageStore.h"
|
||||
|
||||
#include "SafeFile.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
// Hard limits on how much message data to write to flash
|
||||
// Avoid filling the storage if something goes wrong
|
||||
// Normal usage should be well below this size
|
||||
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
|
||||
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
|
||||
|
||||
InkHUD::MessageStore::MessageStore(const std::string &label)
|
||||
{
|
||||
filename = "";
|
||||
filename += "/NicheGraphics";
|
||||
filename += "/";
|
||||
filename += label;
|
||||
filename += ".msgs";
|
||||
}
|
||||
|
||||
// Write the contents of the MessageStore::messages object to flash
|
||||
// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card.
|
||||
// Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally
|
||||
void InkHUD::MessageStore::saveToFlash()
|
||||
{
|
||||
assert(!filename.empty());
|
||||
|
||||
#ifdef FSCom
|
||||
// Make the directory, if doesn't already exist
|
||||
// This is the same directory accessed by NicheGraphics::FlashData
|
||||
spiLock->lock();
|
||||
FSCom.mkdir("/NicheGraphics");
|
||||
spiLock->unlock();
|
||||
|
||||
// Open or create the file
|
||||
// No "full atomic": don't save then rename
|
||||
auto f = SafeFile(filename.c_str(), false);
|
||||
|
||||
LOG_INFO("Saving messages in %s", filename.c_str());
|
||||
|
||||
// Take firmware's SPI Lock while writing
|
||||
spiLock->lock();
|
||||
|
||||
// 1st byte: how many messages will be written to store
|
||||
f.write(messages.size());
|
||||
|
||||
// For each message
|
||||
for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) {
|
||||
Message &m = messages.at(i);
|
||||
f.write(reinterpret_cast<const uint8_t *>(&m.timestamp), sizeof(m.timestamp)); // Write timestamp. 4 bytes
|
||||
f.write(reinterpret_cast<const uint8_t *>(&m.sender), sizeof(m.sender)); // Write sender NodeId. 4 Bytes
|
||||
f.write(reinterpret_cast<const uint8_t *>(&m.channelIndex), sizeof(m.channelIndex)); // Write channel index. 1 Byte
|
||||
f.write(reinterpret_cast<const uint8_t *>(m.text.c_str()),
|
||||
min((size_t)MAX_MESSAGE_SIZE, m.text.size())); // Write message text
|
||||
f.write('\0'); // Append null term
|
||||
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", static_cast<uint32_t>(i),
|
||||
min((size_t)MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str());
|
||||
}
|
||||
|
||||
// Release firmware's SPI lock, because SafeFile::close needs it
|
||||
spiLock->unlock();
|
||||
|
||||
bool writeSucceeded = f.close();
|
||||
|
||||
if (!writeSucceeded) {
|
||||
LOG_ERROR("Can't write data!");
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("ERROR: Filesystem not implemented\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
// Attempt to load the previous contents of the MessageStore:message deque from flash.
|
||||
// Filename is controlled by the "label" parameter
|
||||
// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card.
|
||||
void InkHUD::MessageStore::loadFromFlash()
|
||||
{
|
||||
// Hopefully redundant. Initial intention is to only load / save once per boot.
|
||||
messages.clear();
|
||||
|
||||
#ifdef FSCom
|
||||
|
||||
// Take the firmware's SPI Lock, in case filesystem is on SD card
|
||||
concurrency::LockGuard guard(spiLock);
|
||||
|
||||
// Check that the file *does* actually exist
|
||||
if (!FSCom.exists(filename.c_str())) {
|
||||
LOG_WARN("'%s' not found. Using default values", filename.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that the file *does* actually exist
|
||||
if (!FSCom.exists(filename.c_str())) {
|
||||
LOG_INFO("'%s' not found.", filename.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the file
|
||||
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
|
||||
|
||||
if (f.size() == 0) {
|
||||
LOG_INFO("%s is empty", filename.c_str());
|
||||
f.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// If opened, start reading
|
||||
if (f) {
|
||||
LOG_INFO("Loading threaded messages '%s'", filename.c_str());
|
||||
|
||||
// First byte: how many messages are in the flash store
|
||||
uint8_t flashMessageCount = 0;
|
||||
f.readBytes(reinterpret_cast<char *>(&flashMessageCount), 1);
|
||||
LOG_DEBUG("Messages available: %u", static_cast<uint32_t>(flashMessageCount));
|
||||
|
||||
// For each message
|
||||
for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) {
|
||||
Message m;
|
||||
|
||||
// Read meta data (fixed width)
|
||||
f.readBytes(reinterpret_cast<char *>(&m.timestamp), sizeof(m.timestamp));
|
||||
f.readBytes(reinterpret_cast<char *>(&m.sender), sizeof(m.sender));
|
||||
f.readBytes(reinterpret_cast<char *>(&m.channelIndex), sizeof(m.channelIndex));
|
||||
|
||||
// Read characters until we find a null term
|
||||
char c;
|
||||
while (m.text.size() < MAX_MESSAGE_SIZE) {
|
||||
f.readBytes(&c, 1);
|
||||
if (c != '\0')
|
||||
m.text += c;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
// Store in RAM
|
||||
messages.push_back(m);
|
||||
|
||||
LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", static_cast<uint32_t>(i), m.timestamp, m.sender,
|
||||
m.text.c_str());
|
||||
}
|
||||
|
||||
f.close();
|
||||
} else {
|
||||
LOG_ERROR("Could not open / read %s", filename.c_str());
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("Filesystem not implemented");
|
||||
state = LoadFileState::NO_FILESYSTEM;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,47 +0,0 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
We hold a few recent messages, for features like the threaded message applet.
|
||||
This class contains a struct for storing those messages,
|
||||
and methods for serializing them to flash.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include <deque>
|
||||
|
||||
#include "mesh/MeshTypes.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class MessageStore
|
||||
{
|
||||
public:
|
||||
// A stored message
|
||||
struct Message {
|
||||
uint32_t timestamp; // Epoch seconds
|
||||
NodeNum sender = 0;
|
||||
uint8_t channelIndex;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
MessageStore() = delete;
|
||||
explicit MessageStore(const std::string &label); // Label determines filename in flash
|
||||
|
||||
void saveToFlash();
|
||||
void loadFromFlash();
|
||||
|
||||
std::deque<Message> messages; // Interact with this object!
|
||||
|
||||
private:
|
||||
std::string filename;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
@@ -17,23 +17,23 @@ void InkHUD::Persistence::loadSettings()
|
||||
LOG_WARN("Settings version changed. Using defaults");
|
||||
}
|
||||
|
||||
// Load settings and latestMessage data
|
||||
// Rebuild the latestMessage cache from the global messageStore.
|
||||
// Called after messageStore.loadFromFlash() so that the most recent broadcast and DM
|
||||
// are immediately available to applets (DMApplet, AllMessageApplet, NotificationApplet).
|
||||
void InkHUD::Persistence::loadLatestMessage()
|
||||
{
|
||||
// Load previous "latestMessages" data from flash
|
||||
MessageStore store("latest");
|
||||
store.loadFromFlash();
|
||||
|
||||
// Place into latestMessage struct, for convenient access
|
||||
// Number of strings loaded determines whether last message was broadcast or dm
|
||||
if (store.messages.size() == 1) {
|
||||
latestMessage.dm = store.messages.at(0);
|
||||
latestMessage.wasBroadcast = false;
|
||||
} else if (store.messages.size() == 2) {
|
||||
latestMessage.dm = store.messages.at(0);
|
||||
latestMessage.broadcast = store.messages.at(1);
|
||||
latestMessage.wasBroadcast = true;
|
||||
int lastBroadcastPos = -1, lastDMPos = -1, pos = 0;
|
||||
for (const StoredMessage &m : messageStore.getLiveMessages()) {
|
||||
if (m.type == MessageType::BROADCAST) {
|
||||
latestMessage.broadcast = m;
|
||||
lastBroadcastPos = pos;
|
||||
} else if (m.type == MessageType::DM_TO_US) {
|
||||
latestMessage.dm = m;
|
||||
lastDMPos = pos;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
latestMessage.wasBroadcast = (lastBroadcastPos > lastDMPos);
|
||||
}
|
||||
|
||||
// Save the InkHUD settings to flash
|
||||
@@ -42,15 +42,10 @@ void InkHUD::Persistence::saveSettings()
|
||||
FlashData<Settings>::save(&settings, "settings");
|
||||
}
|
||||
|
||||
// Save latestMessage data to flash
|
||||
// Persist all messages via the global messageStore.
|
||||
void InkHUD::Persistence::saveLatestMessage()
|
||||
{
|
||||
// Number of strings saved determines whether last message was broadcast or dm
|
||||
MessageStore store("latest");
|
||||
store.messages.push_back(latestMessage.dm);
|
||||
if (latestMessage.wasBroadcast)
|
||||
store.messages.push_back(latestMessage.broadcast);
|
||||
store.saveToFlash();
|
||||
messageStore.saveToFlash();
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -15,7 +15,7 @@ The save / load mechanism is a shared NicheGraphics feature.
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./InkHUD.h"
|
||||
#include "graphics/niche/InkHUD/MessageStore.h"
|
||||
#include "MessageStore.h"
|
||||
#include "graphics/niche/Utils/FlashData.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
@@ -120,12 +120,12 @@ class Persistence
|
||||
};
|
||||
|
||||
// Most recently received text message
|
||||
// Value is updated by InkHUD::WindowManager, as a courtesy to applets
|
||||
// InkHUD keeps its own latest-message cache for applets.
|
||||
// Value is updated by InkHUD::Events, as a courtesy to applets.
|
||||
// Populated at boot from the global messageStore, then updated live on receive.
|
||||
struct LatestMessage {
|
||||
MessageStore::Message broadcast; // Most recent message received broadcast
|
||||
MessageStore::Message dm; // Most recent received DM
|
||||
bool wasBroadcast; // True if most recent broadcast is newer than most recent dm
|
||||
StoredMessage broadcast; // Most recent broadcast message received
|
||||
StoredMessage dm; // Most recent DM received
|
||||
bool wasBroadcast; // True if most recent broadcast is newer than most recent dm
|
||||
};
|
||||
|
||||
void loadSettings();
|
||||
|
||||
@@ -422,9 +422,11 @@ Stores InkHUD data in flash
|
||||
- settings
|
||||
- most recent text message received (both for broadcast and DM)
|
||||
|
||||
In rare cases, applets may store their own specific data separately (e.g. `ThreadedMessageApplet`)
|
||||
Message history (used by `ThreadedMessageApplet`) is stored by the firmware-wide `MessageStore` (`src/MessageStore.h`), not by `Persistence` directly.
|
||||
|
||||
Data saved only on shutdown / reboot. Not saved if power is removed unexpectedly.
|
||||
Settings are saved only on shutdown / reboot. Not saved if power is removed unexpectedly.
|
||||
|
||||
Message history is saved periodically (every 2 hours by default), as well as on shutdown / reboot.
|
||||
|
||||
---
|
||||
|
||||
@@ -466,18 +468,21 @@ Collected here, so various user applets don't all have to store their own copy o
|
||||
|
||||
We keep this separate latest-message cache for this purpose, because:
|
||||
|
||||
- it is cleared by an outgoing text message
|
||||
- we want to store both a recent broadcast and a recent DM
|
||||
- we want to expose both the most recent broadcast and most recent DM independently
|
||||
- applets like `DMApplet` and `NotificationApplet` need quick access without scanning the full message history
|
||||
|
||||
#### How messages reach the store
|
||||
|
||||
Broadcasts and DMs take different paths into `messageStore`:
|
||||
|
||||
- **Broadcasts** — `ThreadedMessageApplet::handleReceived()` calls `messageStore.addFromPacket()`. `Events::onReceiveTextMessage()` then updates `latestMessage.broadcast` separately for fast access by `AllMessageApplet` and `NotificationApplet`.
|
||||
- **DMs** — `ThreadedMessageApplet` skips DMs entirely. `Events::onReceiveTextMessage()` calls `messageStore.addFromPacket()` directly and stores the result in `latestMessage.dm`.
|
||||
|
||||
#### Saving / Loading
|
||||
|
||||
_A bit of a hack.._
|
||||
Stored to flash using `InkHUD::MessageStore`, which is really intended for storing a thread of messages (see `ThreadedMessageApplet`). Used because it stores strings more efficiently than `FlashData.h`.
|
||||
The `LatestMessage` cache is not persisted to its own file. On boot, `InkHUD::begin()` calls `messageStore.loadFromFlash()` first, then `Persistence::loadLatestMessage()` rebuilds the cache by scanning the loaded messages for the most recent broadcast and DM.
|
||||
|
||||
The hack is:
|
||||
|
||||
- If most recent message was a DM, we only store the DM.
|
||||
- If most recent message was a broadcast, we store both a DM and a broadcast. The DM may be 0-length string.
|
||||
Text is stored in the firmware-wide shared text pool. Use `MessageStore::getText(msg)` to retrieve it from a `StoredMessage`.
|
||||
|
||||
---
|
||||
|
||||
@@ -582,13 +587,17 @@ Handles events which impact the InkHUD system generally (e.g. shutdown, button p
|
||||
|
||||
Applets themselves do also listen separately for various events, but for the purpose of gathering information which they would like to display.
|
||||
|
||||
#### Text Messages
|
||||
|
||||
`Events::onReceiveTextMessage()` is the central handler for all incoming text messages. It updates the `LatestMessage` cache and, for DMs, also adds the message to `messageStore` (since `ThreadedMessageApplet` only handles broadcasts). See `Persistence::LatestMessage` for details on how the two message types are stored.
|
||||
|
||||
#### Buttons
|
||||
|
||||
Button input is sometimes handled by a system applet. `InkHUD::Events` determines whether the button should be handled by a specific system applet, or should instead trigger a default behavior
|
||||
|
||||
#### Factory Reset
|
||||
|
||||
The Events class handles the admin messages(s) which trigger factory reset. We set `Events::eraseOnReboot = true`, which causes `Events::onReboot` to erase the contents of InkHUD's data directory. We do this because some applets (e.g. ThreadedMessageApplet) save their own data to flash, so if we erased earlier, that data would get re-written during reboot.
|
||||
The Events class handles the admin message(s) which trigger factory reset. We set `Events::eraseOnReboot = true`, which causes `Events::onReboot` to erase the contents of InkHUD's data directory (`/NicheGraphics/`) and also call `messageStore.clearAllMessages()` to wipe the firmware-wide message store (`/Messages_default.msgs`). Both are cleared during reboot rather than earlier, to avoid data being re-written by applets still running before shutdown.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+24
-1
@@ -136,6 +136,10 @@ void printPartitionTable()
|
||||
#include "motion/AccelerometerThread.h"
|
||||
AccelerometerThread *accelerometerThread = nullptr;
|
||||
#endif
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
|
||||
#include "motion/MagnetometerThread.h"
|
||||
MagnetometerThread *magnetometerThread = nullptr;
|
||||
#endif
|
||||
|
||||
#ifdef HAS_I2S
|
||||
#include "AudioThread.h"
|
||||
@@ -206,6 +210,8 @@ bool osk_found = false;
|
||||
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
|
||||
// The I2C address of the Accelerometer (if found)
|
||||
ScanI2C::DeviceAddress accelerometer_found = ScanI2C::ADDRESS_NONE;
|
||||
// The I2C address of the Magnetometer (if found)
|
||||
ScanI2C::DeviceAddress magnetometer_found = ScanI2C::ADDRESS_NONE;
|
||||
// The I2C address of the RGB LED (if found)
|
||||
ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, ScanI2C::ADDRESS_NONE);
|
||||
/// The I2C address of our Air Quality Indicator (if found)
|
||||
@@ -431,6 +437,11 @@ void setup()
|
||||
digitalWrite(VEXT_ENABLE, VEXT_ON_VALUE); // turn on the display power
|
||||
#endif
|
||||
|
||||
#if defined(PIN_SENSOR_EN)
|
||||
pinMode(PIN_SENSOR_EN, OUTPUT);
|
||||
digitalWrite(PIN_SENSOR_EN, PIN_SENSOR_EN_ACTIVE); // turn on sensor power
|
||||
#endif
|
||||
|
||||
#if defined(BIAS_T_ENABLE)
|
||||
pinMode(BIAS_T_ENABLE, OUTPUT);
|
||||
digitalWrite(BIAS_T_ENABLE, BIAS_T_VALUE); // turn on 5V for GPS Antenna
|
||||
@@ -671,6 +682,11 @@ void setup()
|
||||
accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found;
|
||||
LOG_DEBUG("acc_info = %i", acc_info.type);
|
||||
#endif
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_MAGNETOMETER
|
||||
auto mag_info = i2cScanner->firstMagnetometer();
|
||||
magnetometer_found = mag_info.type != ScanI2C::DeviceType::NONE ? mag_info.address : magnetometer_found;
|
||||
LOG_DEBUG("mag_info = %i", mag_info.type);
|
||||
#endif
|
||||
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA260, meshtastic_TelemetrySensorType_INA260);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA226, meshtastic_TelemetrySensorType_INA226);
|
||||
@@ -683,6 +699,8 @@ void setup()
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MMC5983MA, meshtastic_TelemetrySensorType_MMC5983MA);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM42607P, meshtastic_TelemetrySensorType_ICM42607P);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102);
|
||||
@@ -766,6 +784,11 @@ void setup()
|
||||
accelerometerThread = new AccelerometerThread(acc_info.type);
|
||||
}
|
||||
#endif
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_MAGNETOMETER
|
||||
if (mag_info.type != ScanI2C::DeviceType::NONE) {
|
||||
magnetometerThread = new MagnetometerThread(mag_info.type);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(HAS_NEOPIXEL) || defined(UNPHONE) || defined(RGBLED_RED)
|
||||
ambientLightingThread = new AmbientLightingThread(ScanI2C::DeviceType::NONE);
|
||||
@@ -1220,7 +1243,7 @@ void loop()
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE
|
||||
#if (HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)) && ENABLE_MESSAGE_PERSISTENCE
|
||||
messageStoreAutosaveTick();
|
||||
#endif
|
||||
long delayMsec = mainController.runOrDelay();
|
||||
|
||||
@@ -39,6 +39,7 @@ extern bool kb_found;
|
||||
extern bool osk_found;
|
||||
extern ScanI2C::DeviceAddress rtc_found;
|
||||
extern ScanI2C::DeviceAddress accelerometer_found;
|
||||
extern ScanI2C::DeviceAddress magnetometer_found;
|
||||
extern ScanI2C::FoundDevice rgb_found;
|
||||
extern ScanI2C::DeviceAddress aqi_found;
|
||||
|
||||
@@ -73,6 +74,10 @@ extern graphics::Screen *screen;
|
||||
#include "motion/AccelerometerThread.h"
|
||||
extern AccelerometerThread *accelerometerThread;
|
||||
#endif
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
|
||||
#include "motion/MagnetometerThread.h"
|
||||
extern MagnetometerThread *magnetometerThread;
|
||||
#endif
|
||||
|
||||
extern bool isVibrating;
|
||||
|
||||
|
||||
@@ -60,6 +60,26 @@ uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t d
|
||||
return base * coef;
|
||||
}
|
||||
|
||||
uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes,
|
||||
TrafficType type)
|
||||
{
|
||||
uint32_t baseMs = getConfiguredOrDefaultMsScaled(configured, defaultValue, numOnlineNodes);
|
||||
|
||||
if (!myRegion || !myRegion->profile)
|
||||
return baseMs;
|
||||
|
||||
int8_t throttle =
|
||||
(type == TrafficType::POSITION) ? myRegion->profile->positionThrottle : myRegion->profile->telemetryThrottle;
|
||||
|
||||
// throttle <= 0 means unset; 1 is the neutral multiplier — skip the multiply for performance
|
||||
if (throttle <= 1)
|
||||
return baseMs;
|
||||
|
||||
constexpr uint32_t MAX_MS = static_cast<uint32_t>(INT32_MAX);
|
||||
uint64_t result = static_cast<uint64_t>(baseMs) * static_cast<uint64_t>(throttle);
|
||||
return result >= static_cast<uint64_t>(MAX_MS) ? MAX_MS : static_cast<uint32_t>(result);
|
||||
}
|
||||
|
||||
uint32_t Default::getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue)
|
||||
{
|
||||
// If zero, intervals should be coalesced later by getConfiguredOrDefault... methods
|
||||
|
||||
@@ -31,10 +31,18 @@
|
||||
#define min_neighbor_info_broadcast_secs 4 * 60 * 60
|
||||
#define default_map_publish_interval_secs 60 * 60
|
||||
|
||||
enum class TrafficType { POSITION, TELEMETRY };
|
||||
|
||||
// Traffic management defaults
|
||||
#define default_traffic_mgmt_position_precision_bits 24 // ~10m grid cells
|
||||
#define default_traffic_mgmt_position_min_interval_secs (ONE_DAY / 2) // 12 hours between identical positions
|
||||
|
||||
// Hop scaling defaults
|
||||
#define default_hop_scaling_min_target_nodes 40 // walk threshold: first hop reaching this cumulative count
|
||||
#define default_hop_scaling_max_target_nodes 80 // generous extension ceiling (2 × min)
|
||||
#define default_hop_scaling_min_target_nodes_floor 5 // minimum allowed min_target_nodes
|
||||
#define default_hop_scaling_max_target_nodes_ceiling 512 // maximum allowed max_target_nodes
|
||||
|
||||
#ifdef USERPREFS_RINGTONE_NAG_SECS
|
||||
#define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS
|
||||
#else
|
||||
@@ -64,6 +72,8 @@ class Default
|
||||
// Note: numOnlineNodes uses uint32_t to match the public API and allow flexibility,
|
||||
// even though internal node counts use uint16_t (max 65535 nodes)
|
||||
static uint32_t getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes);
|
||||
static uint32_t getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes,
|
||||
TrafficType type);
|
||||
static uint8_t getConfiguredOrDefaultHopLimit(uint8_t configured);
|
||||
static uint32_t getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue);
|
||||
|
||||
|
||||
@@ -368,4 +368,10 @@ template <typename T> bool LR11x0Interface<T>::sleep()
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T> int16_t LR11x0Interface<T>::getCurrentRSSI()
|
||||
{
|
||||
float rssi = lora.getRSSI(false, true);
|
||||
return (int16_t)round(rssi);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -37,6 +37,8 @@ template <class T> class LR11x0Interface : public RadioLibInterface
|
||||
*/
|
||||
T lora;
|
||||
|
||||
int16_t getCurrentRSSI() override;
|
||||
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
|
||||
@@ -136,17 +136,6 @@ template <typename T> bool LR20x0Interface<T>::init()
|
||||
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
|
||||
return false;
|
||||
|
||||
// Some basic info about the module's explicit firmware version - no other info available
|
||||
// Currently requires radiolib godmode
|
||||
|
||||
#if RADIOLIB_GODMODE
|
||||
uint8_t fwMajor = 0;
|
||||
uint8_t fwMinor = 0;
|
||||
int versionRes = lora.getVersion(&fwMajor, &fwMinor);
|
||||
if (versionRes == RADIOLIB_ERR_NONE)
|
||||
LOG_DEBUG("LR20x0 FW %d.%d", fwMajor, fwMinor);
|
||||
#endif
|
||||
|
||||
LOG_INFO("Frequency set to %f", getFreq());
|
||||
LOG_INFO("Bandwidth set to %f", bw);
|
||||
LOG_INFO("Power output set to %d", power);
|
||||
@@ -154,17 +143,6 @@ template <typename T> bool LR20x0Interface<T>::init()
|
||||
if (res == RADIOLIB_ERR_NONE)
|
||||
res = lora.setCRC(2);
|
||||
|
||||
// Standard DCDC ramp timing from RadioLib workarounds (register 0x00F20024)
|
||||
// Currently requires radiolib godmode
|
||||
#if RADIOLIB_GODMODE
|
||||
if (res == RADIOLIB_ERR_NONE) {
|
||||
uint8_t rampTimes[4] = {15, 15, 15, 15}; // Standard case for all conditions
|
||||
res = lora.setRegMode(RADIOLIB_LR2021_REG_MODE_SIMO_NORMAL, rampTimes);
|
||||
if (res != RADIOLIB_ERR_NONE)
|
||||
LOG_WARN("LR2021 setRegMode failed: %d", res);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef LR2021_DIO_AS_RF_SWITCH
|
||||
bool dioAsRfSwitch = true;
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
@@ -396,4 +374,10 @@ template <typename T> bool LR20x0Interface<T>::sleep()
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T> int16_t LR20x0Interface<T>::getCurrentRSSI()
|
||||
{
|
||||
float rssi = lora.getRSSI(false, true);
|
||||
return (int16_t)round(rssi);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -37,6 +37,8 @@ template <class T> class LR20x0Interface : public RadioLibInterface
|
||||
*/
|
||||
T lora;
|
||||
|
||||
int16_t getCurrentRSSI() override;
|
||||
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
|
||||
@@ -2,11 +2,58 @@
|
||||
#include "LoRaFEMInterface.h"
|
||||
|
||||
#if defined(ARCH_ESP32)
|
||||
#include <driver/gpio.h>
|
||||
#include <driver/rtc_io.h>
|
||||
#include <esp_sleep.h>
|
||||
#endif
|
||||
|
||||
LoRaFEMInterface loraFEMInterface;
|
||||
|
||||
static void enableFEMPower()
|
||||
{
|
||||
bool wasOff = digitalRead(LORA_PA_POWER) != HIGH;
|
||||
digitalWrite(LORA_PA_POWER, HIGH);
|
||||
if (wasOff) {
|
||||
delay(5); // This is an arbitrary 5ms for FEM rail power-up.
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(ARCH_ESP32)
|
||||
static void releasePinHold(int pin)
|
||||
{
|
||||
if (pin < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
gpio_num_t gpio = (gpio_num_t)pin;
|
||||
|
||||
#if SOC_RTCIO_HOLD_SUPPORTED
|
||||
if (rtc_gpio_is_valid_gpio(gpio)) {
|
||||
rtc_gpio_hold_dis(gpio);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (GPIO_IS_VALID_OUTPUT_GPIO(gpio)) {
|
||||
gpio_hold_dis(gpio);
|
||||
}
|
||||
}
|
||||
|
||||
static void releaseSleepHolds()
|
||||
{
|
||||
releasePinHold(LORA_PA_POWER);
|
||||
#ifdef HELTEC_V4
|
||||
releasePinHold(LORA_KCT8103L_PA_CSD);
|
||||
releasePinHold(LORA_KCT8103L_PA_CTX);
|
||||
#elif defined(USE_GC1109_PA)
|
||||
releasePinHold(LORA_GC1109_PA_EN);
|
||||
releasePinHold(LORA_GC1109_PA_TX_EN);
|
||||
#elif defined(USE_KCT8103L_PA)
|
||||
releasePinHold(LORA_KCT8103L_PA_CSD);
|
||||
releasePinHold(LORA_KCT8103L_PA_CTX);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
void LoRaFEMInterface::init(void)
|
||||
{
|
||||
setLnaCanControl(false); // Default is uncontrollable
|
||||
@@ -21,6 +68,7 @@ void LoRaFEMInterface::init(void)
|
||||
if (digitalRead(LORA_KCT8103L_PA_CSD) == HIGH) {
|
||||
// FEM is KCT8103L
|
||||
fem_type = KCT8103L_PA;
|
||||
LOG_INFO("Detected KCT8103L LoRa FEM");
|
||||
rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CTX);
|
||||
pinMode(LORA_KCT8103L_PA_CSD, OUTPUT);
|
||||
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
|
||||
@@ -30,6 +78,7 @@ void LoRaFEMInterface::init(void)
|
||||
} else if (digitalRead(LORA_KCT8103L_PA_CSD) == LOW) {
|
||||
// FEM is GC1109
|
||||
fem_type = GC1109_PA;
|
||||
LOG_INFO("Detected GC1109 LoRa FEM");
|
||||
// LORA_GC1109_PA_EN and LORA_KCT8103L_PA_CSD are the same pin and do not need to be repeatedly turned off and held.
|
||||
// rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_EN);
|
||||
pinMode(LORA_GC1109_PA_EN, OUTPUT);
|
||||
@@ -41,6 +90,7 @@ void LoRaFEMInterface::init(void)
|
||||
}
|
||||
#elif defined(USE_GC1109_PA)
|
||||
fem_type = GC1109_PA;
|
||||
LOG_INFO("Using GC1109 LoRa FEM");
|
||||
pinMode(LORA_PA_POWER, OUTPUT);
|
||||
digitalWrite(LORA_PA_POWER, HIGH);
|
||||
#if defined(ARCH_ESP32)
|
||||
@@ -55,6 +105,7 @@ void LoRaFEMInterface::init(void)
|
||||
digitalWrite(LORA_GC1109_PA_TX_EN, LOW);
|
||||
#elif defined(USE_KCT8103L_PA)
|
||||
fem_type = KCT8103L_PA;
|
||||
LOG_INFO("Using KCT8103L LoRa FEM");
|
||||
pinMode(LORA_PA_POWER, OUTPUT);
|
||||
digitalWrite(LORA_PA_POWER, HIGH);
|
||||
#if defined(ARCH_ESP32)
|
||||
@@ -73,6 +124,10 @@ void LoRaFEMInterface::init(void)
|
||||
|
||||
void LoRaFEMInterface::setSleepModeEnable(void)
|
||||
{
|
||||
#if defined(ARCH_ESP32)
|
||||
releaseSleepHolds();
|
||||
#endif
|
||||
|
||||
#ifdef HELTEC_V4
|
||||
if (fem_type == GC1109_PA) {
|
||||
/*
|
||||
@@ -84,6 +139,7 @@ void LoRaFEMInterface::setSleepModeEnable(void)
|
||||
} else if (fem_type == KCT8103L_PA) {
|
||||
// shutdown the PA
|
||||
digitalWrite(LORA_KCT8103L_PA_CSD, LOW);
|
||||
digitalWrite(LORA_PA_POWER, LOW);
|
||||
}
|
||||
#elif defined(USE_GC1109_PA)
|
||||
digitalWrite(LORA_GC1109_PA_EN, LOW);
|
||||
@@ -91,16 +147,22 @@ void LoRaFEMInterface::setSleepModeEnable(void)
|
||||
#elif defined(USE_KCT8103L_PA)
|
||||
// shutdown the PA
|
||||
digitalWrite(LORA_KCT8103L_PA_CSD, LOW);
|
||||
digitalWrite(LORA_PA_POWER, LOW);
|
||||
#endif
|
||||
}
|
||||
|
||||
void LoRaFEMInterface::setTxModeEnable(void)
|
||||
{
|
||||
#if defined(ARCH_ESP32)
|
||||
releaseSleepHolds();
|
||||
#endif
|
||||
|
||||
#ifdef HELTEC_V4
|
||||
if (fem_type == GC1109_PA) {
|
||||
digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled
|
||||
digitalWrite(LORA_GC1109_PA_TX_EN, HIGH); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care)
|
||||
} else if (fem_type == KCT8103L_PA) {
|
||||
enableFEMPower();
|
||||
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
|
||||
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
|
||||
}
|
||||
@@ -108,6 +170,7 @@ void LoRaFEMInterface::setTxModeEnable(void)
|
||||
digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled
|
||||
digitalWrite(LORA_GC1109_PA_TX_EN, HIGH); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care)
|
||||
#elif defined(USE_KCT8103L_PA)
|
||||
enableFEMPower();
|
||||
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
|
||||
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
|
||||
#endif
|
||||
@@ -115,11 +178,16 @@ void LoRaFEMInterface::setTxModeEnable(void)
|
||||
|
||||
void LoRaFEMInterface::setRxModeEnable(void)
|
||||
{
|
||||
#if defined(ARCH_ESP32)
|
||||
releaseSleepHolds();
|
||||
#endif
|
||||
|
||||
#ifdef HELTEC_V4
|
||||
if (fem_type == GC1109_PA) {
|
||||
digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled
|
||||
digitalWrite(LORA_GC1109_PA_TX_EN, LOW);
|
||||
} else if (fem_type == KCT8103L_PA) {
|
||||
enableFEMPower();
|
||||
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
|
||||
if (lna_enabled) {
|
||||
digitalWrite(LORA_KCT8103L_PA_CTX, LOW);
|
||||
@@ -131,6 +199,7 @@ void LoRaFEMInterface::setRxModeEnable(void)
|
||||
digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled
|
||||
digitalWrite(LORA_GC1109_PA_TX_EN, LOW);
|
||||
#elif defined(USE_KCT8103L_PA)
|
||||
enableFEMPower();
|
||||
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
|
||||
if (lna_enabled) {
|
||||
digitalWrite(LORA_KCT8103L_PA_CTX, LOW);
|
||||
@@ -142,12 +211,14 @@ void LoRaFEMInterface::setRxModeEnable(void)
|
||||
|
||||
void LoRaFEMInterface::setRxModeEnableWhenMCUSleep(void)
|
||||
{
|
||||
#if defined(ARCH_ESP32)
|
||||
releaseSleepHolds();
|
||||
#endif
|
||||
|
||||
#ifdef HELTEC_V4
|
||||
// Keep GC1109 FEM powered during deep sleep so LNA remains active for RX wake.
|
||||
// Set PA_POWER and PA_EN HIGH (overrides SX126xInterface::sleep() shutdown),
|
||||
// then latch with RTC hold so the state survives deep sleep.
|
||||
digitalWrite(LORA_PA_POWER, HIGH);
|
||||
// Keep FEM rail powered during deep sleep so LoRa RX wake can work (GC1109 keeps LNA active; KCT8103L uses RX bypass).
|
||||
// Set PA_POWER HIGH (overrides SX126xInterface::sleep() shutdown), then latch with RTC hold so the state survives deep sleep.
|
||||
enableFEMPower();
|
||||
rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER);
|
||||
if (fem_type == GC1109_PA) {
|
||||
digitalWrite(LORA_GC1109_PA_EN, HIGH);
|
||||
@@ -156,15 +227,11 @@ void LoRaFEMInterface::setRxModeEnableWhenMCUSleep(void)
|
||||
} else if (fem_type == KCT8103L_PA) {
|
||||
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
|
||||
rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD);
|
||||
if (lna_enabled) {
|
||||
digitalWrite(LORA_KCT8103L_PA_CTX, LOW);
|
||||
} else {
|
||||
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
|
||||
}
|
||||
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); // RX bypass while MCU sleeps
|
||||
rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX);
|
||||
}
|
||||
#elif defined(USE_GC1109_PA)
|
||||
digitalWrite(LORA_PA_POWER, HIGH);
|
||||
enableFEMPower();
|
||||
digitalWrite(LORA_GC1109_PA_EN, HIGH);
|
||||
#if defined(ARCH_ESP32)
|
||||
rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER);
|
||||
@@ -172,13 +239,11 @@ void LoRaFEMInterface::setRxModeEnableWhenMCUSleep(void)
|
||||
gpio_pulldown_en((gpio_num_t)LORA_GC1109_PA_TX_EN);
|
||||
#endif
|
||||
#elif defined(USE_KCT8103L_PA)
|
||||
enableFEMPower();
|
||||
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
|
||||
if (lna_enabled) {
|
||||
digitalWrite(LORA_KCT8103L_PA_CTX, LOW);
|
||||
} else {
|
||||
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
|
||||
}
|
||||
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); // RX bypass while MCU sleeps
|
||||
#if defined(ARCH_ESP32)
|
||||
rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER);
|
||||
rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD);
|
||||
rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX);
|
||||
#endif
|
||||
@@ -227,4 +292,4 @@ int8_t LoRaFEMInterface::powerConversion(int8_t loraOutputPower)
|
||||
return loraOutputPower;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -244,13 +244,10 @@ void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to)
|
||||
p->decoded.request_id = to.id;
|
||||
}
|
||||
|
||||
std::vector<MeshModule *> MeshModule::GetMeshModulesWithUIFrames(int startIndex)
|
||||
std::vector<MeshModule *> MeshModule::GetMeshModulesWithUIFrames()
|
||||
{
|
||||
std::vector<MeshModule *> modulesWithUIFrames;
|
||||
|
||||
// Fill with nullptr up to startIndex
|
||||
modulesWithUIFrames.resize(startIndex, nullptr);
|
||||
|
||||
if (modules) {
|
||||
for (auto i = modules->begin(); i != modules->end(); ++i) {
|
||||
auto &pi = **i;
|
||||
@@ -311,4 +308,4 @@ bool MeshModule::isRequestingFocus()
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -76,7 +76,7 @@ class MeshModule
|
||||
*/
|
||||
static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO);
|
||||
|
||||
static std::vector<MeshModule *> GetMeshModulesWithUIFrames(int startIndex);
|
||||
static std::vector<MeshModule *> GetMeshModulesWithUIFrames();
|
||||
static void observeUIEvents(Observer<const UIFrameEvent *> *observer);
|
||||
static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp,
|
||||
meshtastic_AdminMessage *request,
|
||||
|
||||
+83
-15
@@ -13,9 +13,16 @@
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END =
|
||||
static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(0xFF);
|
||||
|
||||
#define PRESET(name) meshtastic_Config_LoRaConfig_ModemPreset_##name
|
||||
|
||||
// Override slot magic numbers for RegionInfo.overrideSlot
|
||||
#define OVERRIDE_SLOT_DEFAULT_CHANNEL_HASH 0 // Use hash of primary channel name
|
||||
#define OVERRIDE_SLOT_PRESET_HASH -1 // Use hash of preset name instead
|
||||
// Positive values (1-32767) are explicit slot numbers
|
||||
|
||||
// Region profile: bundles the preset list with regulatory parameters shared across regions
|
||||
struct RegionProfile {
|
||||
const meshtastic_Config_LoRaConfig_ModemPreset *presets; // sentinel-terminated; first entry is the default
|
||||
const meshtastic_Config_LoRaConfig_ModemPreset *presets; // sentinel-terminated
|
||||
float spacing; // gaps between radio channels
|
||||
float padding; // padding at each side of the "operating channel"
|
||||
bool audioPermitted;
|
||||
@@ -23,15 +30,22 @@ struct RegionProfile {
|
||||
int8_t textThrottle; // throttle for text - future expansion
|
||||
int8_t positionThrottle; // throttle for location data - future expansion
|
||||
int8_t telemetryThrottle; // throttle for telemetry - future expansion
|
||||
uint8_t overrideSlot; // a per-region override slot for if we need to fix it in place
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the effective duty cycle for the current region based on device role.
|
||||
* For EU_866, returns 10% for fixed devices (ROUTER, ROUTER_LATE) and 2.5% for mobile devices.
|
||||
* For other regions, returns the standard duty cycle.
|
||||
*/
|
||||
extern float getEffectiveDutyCycle();
|
||||
|
||||
extern const RegionProfile PROFILE_STD;
|
||||
extern const RegionProfile PROFILE_EU868;
|
||||
extern const RegionProfile PROFILE_UNDEF;
|
||||
// extern const RegionProfile PROFILE_LITE;
|
||||
// extern const RegionProfile PROFILE_NARROW;
|
||||
// extern const RegionProfile PROFILE_HAM;
|
||||
extern const RegionProfile PROFILE_LITE;
|
||||
extern const RegionProfile PROFILE_NARROW;
|
||||
extern const RegionProfile PROFILE_HAM_20KHZ;
|
||||
extern const RegionProfile PROFILE_HAM_100KHZ;
|
||||
|
||||
// Map from old region names to new region enums
|
||||
struct RegionInfo {
|
||||
@@ -43,10 +57,13 @@ struct RegionInfo {
|
||||
bool freqSwitching;
|
||||
bool wideLora;
|
||||
const RegionProfile *profile;
|
||||
const char *name; // EU433 etc
|
||||
meshtastic_Config_LoRaConfig_ModemPreset defaultPreset;
|
||||
int16_t overrideSlot; // per-region override slot for frequency selection
|
||||
// Magic values: 0 = use channel name hash, -1 = use preset name hash, >0 = explicit slot
|
||||
const char *name; // EU433 etc
|
||||
|
||||
// Preset accessors (delegate through profile)
|
||||
meshtastic_Config_LoRaConfig_ModemPreset getDefaultPreset() const { return profile->presets[0]; }
|
||||
meshtastic_Config_LoRaConfig_ModemPreset getDefaultPreset() const { return defaultPreset; }
|
||||
const meshtastic_Config_LoRaConfig_ModemPreset *getAvailablePresets() const { return profile->presets; }
|
||||
size_t getNumPresets() const
|
||||
{
|
||||
@@ -61,6 +78,7 @@ extern const RegionInfo regions[];
|
||||
extern const RegionInfo *myRegion;
|
||||
|
||||
extern void initRegion();
|
||||
extern const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code);
|
||||
|
||||
// Valid LoRa spread factor range and defaults
|
||||
constexpr uint8_t LORA_SF_MIN = 5;
|
||||
@@ -107,8 +125,18 @@ static inline float clampBandwidthKHz(float bwKHz)
|
||||
|
||||
static inline float bwCodeToKHz(uint16_t bwCode)
|
||||
{
|
||||
if (bwCode == 8)
|
||||
return 7.8f;
|
||||
if (bwCode == 10)
|
||||
return 10.4f;
|
||||
if (bwCode == 16)
|
||||
return 15.6f;
|
||||
if (bwCode == 21)
|
||||
return 20.8f;
|
||||
if (bwCode == 31)
|
||||
return 31.25f;
|
||||
if (bwCode == 42)
|
||||
return 41.7f;
|
||||
if (bwCode == 62)
|
||||
return 62.5f;
|
||||
if (bwCode == 200)
|
||||
@@ -124,8 +152,18 @@ static inline float bwCodeToKHz(uint16_t bwCode)
|
||||
|
||||
static inline uint16_t bwKHzToCode(float bwKHz)
|
||||
{
|
||||
if (bwKHz > 7.7f && bwKHz < 7.9f)
|
||||
return 8;
|
||||
if (bwKHz > 10.3f && bwKHz < 10.5f)
|
||||
return 10;
|
||||
if (bwKHz > 15.5f && bwKHz < 15.7f)
|
||||
return 16;
|
||||
if (bwKHz > 20.7f && bwKHz < 20.9f)
|
||||
return 21;
|
||||
if (bwKHz > 31.24f && bwKHz < 31.26f)
|
||||
return 31;
|
||||
if (bwKHz > 41.6f && bwKHz < 41.8f)
|
||||
return 42;
|
||||
if (bwKHz > 62.49f && bwKHz < 62.51f)
|
||||
return 62;
|
||||
if (bwKHz > 203.12f && bwKHz < 203.13f)
|
||||
@@ -143,46 +181,76 @@ static inline void modemPresetToParams(meshtastic_Config_LoRaConfig_ModemPreset
|
||||
uint8_t &cr)
|
||||
{
|
||||
switch (preset) {
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
|
||||
case PRESET(SHORT_TURBO):
|
||||
bwKHz = wideLora ? 1625.0f : 500.0f;
|
||||
cr = 5;
|
||||
sf = 7;
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
|
||||
case PRESET(SHORT_FAST):
|
||||
bwKHz = wideLora ? 812.5f : 250.0f;
|
||||
cr = 5;
|
||||
sf = 7;
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
|
||||
case PRESET(SHORT_SLOW):
|
||||
bwKHz = wideLora ? 812.5f : 250.0f;
|
||||
cr = 5;
|
||||
sf = 8;
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
|
||||
case PRESET(MEDIUM_FAST):
|
||||
bwKHz = wideLora ? 812.5f : 250.0f;
|
||||
cr = 5;
|
||||
sf = 9;
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
|
||||
case PRESET(MEDIUM_SLOW):
|
||||
bwKHz = wideLora ? 812.5f : 250.0f;
|
||||
cr = 5;
|
||||
sf = 10;
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
|
||||
case PRESET(LONG_TURBO):
|
||||
bwKHz = wideLora ? 1625.0f : 500.0f;
|
||||
cr = 8;
|
||||
sf = 11;
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
|
||||
case PRESET(LONG_MODERATE):
|
||||
bwKHz = wideLora ? 406.25f : 125.0f;
|
||||
cr = 8;
|
||||
sf = 11;
|
||||
break;
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
|
||||
case PRESET(LONG_SLOW):
|
||||
bwKHz = wideLora ? 406.25f : 125.0f;
|
||||
cr = 8;
|
||||
sf = 12;
|
||||
break;
|
||||
case PRESET(LITE_FAST):
|
||||
bwKHz = 125;
|
||||
cr = 5;
|
||||
sf = 9;
|
||||
break;
|
||||
case PRESET(LITE_SLOW):
|
||||
bwKHz = 125;
|
||||
cr = 5;
|
||||
sf = 10;
|
||||
break;
|
||||
case PRESET(NARROW_FAST):
|
||||
bwKHz = 62.5f;
|
||||
cr = 6;
|
||||
sf = 7;
|
||||
break;
|
||||
case PRESET(NARROW_SLOW):
|
||||
bwKHz = 62.5f;
|
||||
cr = 6;
|
||||
sf = 8;
|
||||
break;
|
||||
case PRESET(TINY_FAST):
|
||||
bwKHz = 15.6f;
|
||||
cr = 5;
|
||||
sf = 7;
|
||||
break;
|
||||
case PRESET(TINY_SLOW):
|
||||
bwKHz = 15.6f;
|
||||
cr = 6;
|
||||
sf = 8;
|
||||
break;
|
||||
default: // LONG_FAST (or illegal)
|
||||
bwKHz = wideLora ? 812.5f : 250.0f;
|
||||
cr = 5;
|
||||
|
||||
+20
-1
@@ -25,6 +25,9 @@
|
||||
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/NeighborInfoModule.h"
|
||||
#if HAS_VARIABLE_HOPS
|
||||
#include "modules/HopScalingModule.h"
|
||||
#endif
|
||||
#include "xmodem.h"
|
||||
#include <ErriezCRC32.h>
|
||||
#include <algorithm>
|
||||
@@ -927,7 +930,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
|
||||
strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32);
|
||||
|
||||
#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \
|
||||
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT) || defined(ELECROW_ThinkNode_M9)) && \
|
||||
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT)) && \
|
||||
HAS_TFT
|
||||
// switch BT off by default; use TFT programming mode or hotkey to enable
|
||||
config.bluetooth.enabled = false;
|
||||
@@ -1986,6 +1989,14 @@ bool NodeDB::saveDeviceStateToDisk()
|
||||
|
||||
bool NodeDB::saveNodeDatabaseToDisk()
|
||||
{
|
||||
// Don't persist the node DB until this device has a PKI keypair
|
||||
// TODO: revisit when https://github.com/meshtastic/firmware/pull/10478 lands
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
||||
if (owner.public_key.size != 32 && !owner.is_licensed) {
|
||||
LOG_DEBUG("Skip NodeDB without key");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// 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.
|
||||
@@ -2538,6 +2549,14 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
|
||||
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_VIA_MQTT_MASK,
|
||||
mp.via_mqtt); // Store if we received this packet via MQTT
|
||||
|
||||
#if HAS_VARIABLE_HOPS
|
||||
// Only sample packets that arrived over LoRa.
|
||||
if (mp.transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && hopScalingModule) {
|
||||
uint8_t hopCount = std::max(int8_t(0), getHopsAway(mp));
|
||||
hopScalingModule->samplePacketForHistogram(mp.from, hopCount);
|
||||
}
|
||||
#endif
|
||||
|
||||
// If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway
|
||||
const int8_t hopsAway = getHopsAway(mp);
|
||||
if (hopsAway >= 0) {
|
||||
|
||||
@@ -4,17 +4,19 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex)
|
||||
uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel)
|
||||
{
|
||||
const meshtastic_Channel &channel = channels.getByIndex(channelIndex);
|
||||
|
||||
if (channel.settings.has_module_settings) {
|
||||
return channel.settings.module_settings.position_precision;
|
||||
} else if (channel.role == meshtastic_Channel_Role_PRIMARY) {
|
||||
return 32;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
// No module settings: fail closed. A PRIMARY channel used to default to 32
|
||||
// here, leaking an exact position on a sharing-disabled channel. See #10509.
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex)
|
||||
{
|
||||
return getPositionPrecisionForChannel(channels.getByIndex(channelIndex));
|
||||
}
|
||||
|
||||
static int32_t truncateCoordinate(int32_t coordinate, uint32_t precision)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "meshtastic/channel.pb.h"
|
||||
#include "meshtastic/mesh.pb.h"
|
||||
#include <stdint.h>
|
||||
|
||||
uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel);
|
||||
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex);
|
||||
void applyPositionPrecision(meshtastic_Position &position, uint32_t precision);
|
||||
bool applyPositionPrecision(meshtastic_MeshPacket &packet, uint32_t precision);
|
||||
|
||||
@@ -341,4 +341,10 @@ bool RF95Interface::sleep()
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int16_t RF95Interface::getCurrentRSSI()
|
||||
{
|
||||
float rssi = lora->getRSSI(false);
|
||||
return (int16_t)round(rssi);
|
||||
}
|
||||
#endif
|
||||
@@ -37,6 +37,8 @@ class RF95Interface : public RadioLibInterface
|
||||
*/
|
||||
virtual void disableInterrupt() override;
|
||||
|
||||
int16_t getCurrentRSSI() override;
|
||||
|
||||
/**
|
||||
* Enable a particular ISR callback glue function
|
||||
*/
|
||||
|
||||
+159
-60
@@ -35,31 +35,39 @@
|
||||
#endif
|
||||
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_STD[] = {
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, MODEM_PRESET_END};
|
||||
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST), PRESET(SHORT_SLOW),
|
||||
PRESET(SHORT_FAST), PRESET(LONG_MODERATE), PRESET(SHORT_TURBO), PRESET(LONG_TURBO), MODEM_PRESET_END};
|
||||
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_EU_868[] = {
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, MODEM_PRESET_END};
|
||||
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST),
|
||||
PRESET(SHORT_SLOW), PRESET(SHORT_FAST), PRESET(LONG_MODERATE), MODEM_PRESET_END};
|
||||
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_UNDEF[] = {meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
|
||||
MODEM_PRESET_END};
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_UNDEF[] = {PRESET(LONG_FAST), MODEM_PRESET_END};
|
||||
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_LITE[] = {PRESET(LITE_FAST), PRESET(LITE_SLOW), MODEM_PRESET_END};
|
||||
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_NARROW[] = {PRESET(NARROW_FAST), PRESET(NARROW_SLOW),
|
||||
MODEM_PRESET_END};
|
||||
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_TINY[] = {PRESET(TINY_FAST), PRESET(TINY_SLOW), MODEM_PRESET_END};
|
||||
|
||||
// Region profiles: bundle preset list + regulatory parameters shared across regions
|
||||
// presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle, override slot
|
||||
const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 0, 0, 0};
|
||||
const RegionProfile PROFILE_EU868 = {PRESETS_EU_868, 0, 0, false, false, 0, 0, 0, 0};
|
||||
const RegionProfile PROFILE_UNDEF = {PRESETS_UNDEF, 0, 0, true, false, 0, 0, 0, 0};
|
||||
// presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle
|
||||
const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 1, 1};
|
||||
const RegionProfile PROFILE_EU868 = {PRESETS_EU_868, 0, 0, false, false, 0, 1, 1};
|
||||
const RegionProfile PROFILE_UNDEF = {PRESETS_UNDEF, 0, 0, true, false, 0, 1, 1};
|
||||
const RegionProfile PROFILE_LITE = {PRESETS_LITE, 0.4, 0.0375f, false, false, 0, 10, 10};
|
||||
const RegionProfile PROFILE_NARROW = {PRESETS_NARROW, 0, 0.0104f, true, false, 0, 1, 1};
|
||||
// Ham '20kHz' profile. 15.6kHz bandwidth coerced to 20kHz via padding.
|
||||
const RegionProfile PROFILE_HAM_20KHZ = {PRESETS_TINY, 0, 0.0022f, false, true, 0, 2, 2};
|
||||
// Ham '100kHz' profile. 62.5kHz bandwidth coerced to 100kHz via padding.
|
||||
const RegionProfile PROFILE_HAM_100KHZ = {PRESETS_NARROW, 0, 0.01875f, false, true, 0, 1, 1};
|
||||
|
||||
#define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr) \
|
||||
#define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr, default_preset, \
|
||||
override_slot) \
|
||||
{ \
|
||||
meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, \
|
||||
wide_lora, &profile_ptr, #name \
|
||||
wide_lora, &profile_ptr, default_preset, override_slot, #name \
|
||||
}
|
||||
|
||||
const RegionInfo regions[] = {
|
||||
@@ -67,7 +75,7 @@ const RegionInfo regions[] = {
|
||||
https://link.springer.com/content/pdf/bbm%3A978-1-4842-4357-2%2F1.pdf
|
||||
https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/
|
||||
*/
|
||||
RDEF(US, 902.0f, 928.0f, 100, 30, false, false, PROFILE_STD),
|
||||
RDEF(US, 902.0f, 928.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
EN300220 ETSI V3.2.1 [Table B.1, Item H, p. 21]
|
||||
@@ -75,7 +83,7 @@ const RegionInfo regions[] = {
|
||||
https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.02.01_60/en_30022002v030201p.pdf
|
||||
FIXME: https://github.com/meshtastic/firmware/issues/3371
|
||||
*/
|
||||
RDEF(EU_433, 433.0f, 434.0f, 10, 10, false, false, PROFILE_STD),
|
||||
RDEF(EU_433, 433.0f, 434.0f, 10, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
/*
|
||||
https://www.thethingsnetwork.org/docs/lorawan/duty-cycle/
|
||||
https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/
|
||||
@@ -90,33 +98,33 @@ const RegionInfo regions[] = {
|
||||
AFA) to avoid a duty cycle. (Please refer to line P page 22 of this document.)
|
||||
https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.01.01_60/en_30022002v030101p.pdf
|
||||
*/
|
||||
RDEF(EU_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_EU868),
|
||||
RDEF(EU_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_EU868, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
|
||||
*/
|
||||
RDEF(CN, 470.0f, 510.0f, 100, 19, false, false, PROFILE_STD),
|
||||
RDEF(CN, 470.0f, 510.0f, 100, 19, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
|
||||
https://www.arib.or.jp/english/html/overview/doc/5-STD-T108v1_5-E1.pdf
|
||||
https://qiita.com/ammo0613/items/d952154f1195b64dc29f
|
||||
*/
|
||||
RDEF(JP, 920.5f, 923.5f, 100, 13, false, false, PROFILE_STD),
|
||||
RDEF(JP, 920.5f, 923.5f, 100, 13, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
https://www.iot.org.au/wp/wp-content/uploads/2016/12/IoTSpectrumFactSheet.pdf
|
||||
https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf
|
||||
Also used in Brazil.
|
||||
*/
|
||||
RDEF(ANZ, 915.0f, 928.0f, 100, 30, false, false, PROFILE_STD),
|
||||
RDEF(ANZ, 915.0f, 928.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
433.05 - 434.79 MHz, 25mW EIRP max, No duty cycle restrictions
|
||||
AU Low Interference Potential https://www.acma.gov.au/licences/low-interference-potential-devices-lipd-class-licence
|
||||
NZ General User Radio Licence for Short Range Devices https://gazette.govt.nz/notice/id/2022-go3100
|
||||
*/
|
||||
RDEF(ANZ_433, 433.05f, 434.79f, 100, 14, false, false, PROFILE_STD),
|
||||
RDEF(ANZ_433, 433.05f, 434.79f, 100, 14, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
https://digital.gov.ru/uploaded/files/prilozhenie-12-k-reshenyu-gkrch-18-46-03-1.pdf
|
||||
@@ -124,13 +132,13 @@ const RegionInfo regions[] = {
|
||||
Note:
|
||||
- We do LBT, so 100% is allowed.
|
||||
*/
|
||||
RDEF(RU, 868.7f, 869.2f, 100, 20, false, false, PROFILE_STD),
|
||||
RDEF(RU, 868.7f, 869.2f, 100, 20, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
https://www.law.go.kr/LSW/admRulLsInfoP.do?admRulId=53943&efYd=0
|
||||
https://resources.lora-alliance.org/technical-specifications/rp002-1-0-4-regional-parameters
|
||||
*/
|
||||
RDEF(KR, 920.0f, 923.0f, 100, 23, false, false, PROFILE_STD),
|
||||
RDEF(KR, 920.0f, 923.0f, 100, 23, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
Taiwan, 920-925Mhz, limited to 0.5W indoor or coastal, 1.0W outdoor.
|
||||
@@ -138,40 +146,40 @@ const RegionInfo regions[] = {
|
||||
https://www.ncc.gov.tw/english/files/23070/102_5190_230703_1_doc_C.PDF
|
||||
https://gazette.nat.gov.tw/egFront/e_detail.do?metaid=147283
|
||||
*/
|
||||
RDEF(TW, 920.0f, 925.0f, 100, 27, false, false, PROFILE_STD),
|
||||
RDEF(TW, 920.0f, 925.0f, 100, 27, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
|
||||
*/
|
||||
RDEF(IN, 865.0f, 867.0f, 100, 30, false, false, PROFILE_STD),
|
||||
RDEF(IN, 865.0f, 867.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
https://rrf.rsm.govt.nz/smart-web/smart/page/-smart/domain/licence/LicenceSummary.wdk?id=219752
|
||||
https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf
|
||||
*/
|
||||
RDEF(NZ_865, 864.0f, 868.0f, 100, 36, false, false, PROFILE_STD),
|
||||
RDEF(NZ_865, 864.0f, 868.0f, 100, 36, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
|
||||
https://standard.nbtc.go.th/getattachment/Standards/%E0%B8%A1%E0%B8%B2%E0%B8%95%E0%B8%A3%E0%B8%90%E0%B8%B2%E0%B8%99%E0%B8%97%E0%B8%B2%E0%B8%87%E0%B9%80%E0%B8%97%E0%B8%84%E0%B8%99%E0%B8%B4%E0%B8%84%E0%B8%82%E0%B8%AD%E0%B8%87%E0%B9%80%E0%B8%84%E0%B8%A3%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%87%E0%B9%82%E0%B8%97%E0%B8%A3%E0%B8%84%E0%B8%A1%E0%B8%99%E0%B8%B2%E0%B8%84%E0%B8%A1/1033-2565.pdf.aspx?lang=th-TH
|
||||
Thailand 920–925 MHz set max TX power to 27 dBm and enforce 10% duty cycle, aligned with NBTC regulations.
|
||||
*/
|
||||
RDEF(TH, 920.0f, 925.0f, 10, 27, false, false, PROFILE_STD),
|
||||
RDEF(TH, 920.0f, 925.0f, 10, 27, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
433,05-434,7 Mhz 10 mW
|
||||
868,0-868,6 Mhz 25 mW
|
||||
https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf
|
||||
*/
|
||||
RDEF(UA_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD),
|
||||
RDEF(UA_868, 868.0f, 868.6f, 1, 14, false, false, PROFILE_STD),
|
||||
RDEF(UA_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
RDEF(UA_868, 868.0f, 868.6f, 1, 14, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
Malaysia
|
||||
433 - 435 MHz at 100mW, no restrictions.
|
||||
https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf
|
||||
*/
|
||||
RDEF(MY_433, 433.0f, 435.0f, 100, 20, false, false, PROFILE_STD),
|
||||
RDEF(MY_433, 433.0f, 435.0f, 100, 20, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
Malaysia
|
||||
@@ -180,14 +188,14 @@ const RegionInfo regions[] = {
|
||||
Frequency hopping is used for 919 - 923 MHz.
|
||||
https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf
|
||||
*/
|
||||
RDEF(MY_919, 919.0f, 924.0f, 100, 27, true, false, PROFILE_STD),
|
||||
RDEF(MY_919, 919.0f, 924.0f, 100, 27, true, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
Singapore
|
||||
SG_923 Band 30d: 917 - 925 MHz at 100mW, no restrictions.
|
||||
https://www.imda.gov.sg/-/media/imda/files/regulation-licensing-and-consultations/ict-standards/telecommunication-standards/radio-comms/imdatssrd.pdf
|
||||
*/
|
||||
RDEF(SG_923, 917.0f, 925.0f, 100, 20, false, false, PROFILE_STD),
|
||||
RDEF(SG_923, 917.0f, 925.0f, 100, 20, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
Philippines
|
||||
@@ -197,9 +205,9 @@ const RegionInfo regions[] = {
|
||||
https://github.com/meshtastic/firmware/issues/4948#issuecomment-2394926135
|
||||
*/
|
||||
|
||||
RDEF(PH_433, 433.0f, 434.7f, 100, 10, false, false, PROFILE_STD),
|
||||
RDEF(PH_868, 868.0f, 869.4f, 100, 14, false, false, PROFILE_STD),
|
||||
RDEF(PH_915, 915.0f, 918.0f, 100, 24, false, false, PROFILE_STD),
|
||||
RDEF(PH_433, 433.0f, 434.7f, 100, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
RDEF(PH_868, 868.0f, 869.4f, 100, 14, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
RDEF(PH_915, 915.0f, 918.0f, 100, 24, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
Kazakhstan
|
||||
@@ -207,32 +215,85 @@ const RegionInfo regions[] = {
|
||||
863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields
|
||||
https://github.com/meshtastic/firmware/issues/7204
|
||||
*/
|
||||
RDEF(KZ_433, 433.075f, 434.775f, 100, 10, false, false, PROFILE_STD),
|
||||
RDEF(KZ_863, 863.0f, 868.0f, 100, 30, false, false, PROFILE_STD),
|
||||
RDEF(KZ_433, 433.075f, 434.775f, 100, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
RDEF(KZ_863, 863.0f, 868.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
Nepal
|
||||
865 MHz to 868 MHz frequency band for IoT (Internet of Things), M2M (Machine-to-Machine), and smart metering use,
|
||||
specifically in non-cellular mode. https://www.nta.gov.np/uploads/contents/Radio-Frequency-Policy-2080-English.pdf
|
||||
*/
|
||||
RDEF(NP_865, 865.0f, 868.0f, 100, 30, false, false, PROFILE_STD),
|
||||
RDEF(NP_865, 865.0f, 868.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
Brazil
|
||||
902 - 907.5 MHz , 1W power limit, no duty cycle restrictions
|
||||
https://github.com/meshtastic/firmware/issues/3741
|
||||
*/
|
||||
RDEF(BR_902, 902.0f, 907.5f, 100, 30, false, false, PROFILE_STD),
|
||||
RDEF(BR_902, 902.0f, 907.5f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
ITU Region 1 (Europe, Africa, Middle East, former USSR) amateur 2m allocation: 144.000 - 146.000 MHz.
|
||||
Power limit is the regulatory ceiling (1 W / 30 dBm) — individual hardware will cap below this
|
||||
via its own PA curve; the field here is just the legal upper bound.
|
||||
|
||||
Default slot: 26 (144.510 MHz)
|
||||
https://www.iaru-r1.org/wp-content/uploads/2020/12/VHF-Bandplan.pdf
|
||||
*/
|
||||
RDEF(ITU1_2M, 144.0f, 146.0f, 100, 30, false, false, PROFILE_HAM_20KHZ, PRESET(TINY_FAST), 26),
|
||||
|
||||
/*
|
||||
ITU Region 2 (Americas) amateur 2m allocation: 144.000 - 148.000 MHz.
|
||||
Typical admin rules (e.g. US FCC Part 97) allow well above 30 dBm for licensed operators.
|
||||
|
||||
Default slot: 51 (145.010 MHz)
|
||||
https://www.arrl.org/band-plan
|
||||
*/
|
||||
RDEF(ITU2_2M, 144.0f, 148.0f, 100, 30, false, false, PROFILE_HAM_20KHZ, PRESET(TINY_FAST), 51),
|
||||
|
||||
/*
|
||||
ITU Region 3 (Asia/Pacific) amateur 2m allocation: 144.000 - 148.000 MHz.
|
||||
Typical admin rules allow well above 30 dBm for licensed operators.
|
||||
|
||||
Default slot: 33 (144.650 MHz)
|
||||
https://www.iaru.org/wp-content/uploads/2020/01/R3-004-IARU-Region-3-Bandplan-rev.2.pdf
|
||||
https://www.wia.org.au/members/bandplans/data/documents/WIA%20Australian%20Band%20Plan%202026.pdf
|
||||
*/
|
||||
RDEF(ITU3_2M, 144.0f, 148.0f, 100, 30, false, false, PROFILE_HAM_20KHZ, PRESET(TINY_FAST), 33),
|
||||
|
||||
/*
|
||||
ITU Region 2 (Americas) amateur 1.25m '125cm' allocation: 220.000 - 225.000 MHz.
|
||||
Typical admin rules (e.g. US FCC Part 97) allow well above 30 dBm for licensed operators.
|
||||
Note: Some countries do not allocate 220-222 MHz (e.g. USA, Canada). Check local law!
|
||||
|
||||
Default slot: 37 (223.650 MHz)
|
||||
https://www.arrl.org/band-plan
|
||||
*/
|
||||
RDEF(ITU2_125CM, 220.0f, 225.0f, 100, 30, false, false, PROFILE_HAM_100KHZ, PRESET(NARROW_SLOW), 37),
|
||||
|
||||
/*
|
||||
2.4 GHZ WLAN Band equivalent. Only for SX128x chips.
|
||||
*/
|
||||
RDEF(LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, PROFILE_STD),
|
||||
RDEF(LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
EU 866MHz band (Band no. 46b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD)
|
||||
Gives 4 channels at 865.7/866.3/866.9/867.5 MHz, 400 kHz gap plus 37.5 kHz padding between channels, 27 dBm,
|
||||
duty cycle 2.5% (mobile) or 10% (fixed) https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:02006D0771(01)-20250123
|
||||
*/
|
||||
RDEF(EU_866, 865.6f, 867.6f, 2.5, 27, false, false, PROFILE_LITE, PRESET(LITE_FAST), 0),
|
||||
|
||||
/*
|
||||
EU 868MHz band: 3 channels at 869.410/869.4625/869.577 MHz
|
||||
Channel centres at 869.442/869.525/869.608 MHz,
|
||||
10.4 kHz padding on channels, 27 dBm, duty cycle 10%
|
||||
*/
|
||||
RDEF(EU_N_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_NARROW, PRESET(NARROW_SLOW), 1),
|
||||
|
||||
/*
|
||||
This needs to be last. Same as US.
|
||||
*/
|
||||
RDEF(UNSET, 902.0f, 928.0f, 100, 30, false, false, PROFILE_UNDEF)
|
||||
RDEF(UNSET, 902.0f, 928.0f, 100, 30, false, false, PROFILE_UNDEF, PRESET(LONG_FAST), 0),
|
||||
|
||||
};
|
||||
|
||||
@@ -546,6 +607,23 @@ const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code)
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get duty cycle for current region. EU_866: 10% for routers, 2.5% for mobile.
|
||||
*/
|
||||
float getEffectiveDutyCycle()
|
||||
{
|
||||
if (myRegion->code == meshtastic_Config_LoRaConfig_RegionCode_EU_866) {
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ||
|
||||
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) {
|
||||
return 10.0f;
|
||||
} else {
|
||||
return 2.5f;
|
||||
}
|
||||
}
|
||||
// For all other regions, return the standard duty cycle
|
||||
return myRegion->dutyCycle;
|
||||
}
|
||||
|
||||
uint32_t RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool received)
|
||||
{
|
||||
uint32_t pl = 0;
|
||||
@@ -801,6 +879,16 @@ bool RadioInterface::validateConfigRegion(const meshtastic_Config_LoRaConfig &lo
|
||||
{
|
||||
const RegionInfo *newRegion = getRegion(loraConfig.region);
|
||||
|
||||
// Reject unrecognized region codes (getRegion returns UNSET sentinel for unknown codes)
|
||||
if (newRegion->code != loraConfig.region) {
|
||||
char err_string[160];
|
||||
snprintf(err_string, sizeof(err_string), "Region code %d is not recognized", loraConfig.region);
|
||||
LOG_ERROR("%s", err_string);
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
sendErrorNotification(err_string);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If you are not licensed, you can't use ham regions.
|
||||
if (newRegion->profile->licensedOnly && !devicestate.owner.is_licensed) {
|
||||
char err_string[160];
|
||||
@@ -897,12 +985,15 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo
|
||||
if (loraConfig.override_frequency == 0) {
|
||||
|
||||
// Check if we use the default frequency slot
|
||||
// overrideSlot: 0 = channel hash, -1 = preset hash, >0 = explicit slot
|
||||
uses_default_frequency_slot =
|
||||
(loraConfig.channel_num == 0) || // user choice unset, no frequency override, so use default
|
||||
(newRegion->profile->overrideSlot != 0 &&
|
||||
loraConfig.channel_num == newRegion->profile->overrideSlot) || // user setting matches override
|
||||
((newRegion->profile->overrideSlot == 0) &&
|
||||
((uint32_t)(loraConfig.channel_num - 1) == presetNameHashSlot)); // user setting matches preset hash, no override
|
||||
(newRegion->overrideSlot > 0 &&
|
||||
loraConfig.channel_num == newRegion->overrideSlot) || // user setting matches explicit override slot
|
||||
((newRegion->overrideSlot == OVERRIDE_SLOT_DEFAULT_CHANNEL_HASH) &&
|
||||
((uint32_t)(loraConfig.channel_num - 1) == channelNameHashSlot)) || // user setting matches channel name hash
|
||||
((newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) &&
|
||||
((uint32_t)(loraConfig.channel_num - 1) == presetNameHashSlot)); // user setting matches preset name hash
|
||||
|
||||
// check if user setting different to preset name
|
||||
uses_custom_channel_name = (strcmp(channelName, presetNameDisplay) != 0);
|
||||
@@ -917,10 +1008,13 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo
|
||||
if (clamp) {
|
||||
if (uses_custom_channel_name) { // clamp to channel name hash
|
||||
loraConfig.channel_num =
|
||||
channelNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1
|
||||
} else if ((loraConfig.use_preset) && (newRegion->profile->overrideSlot != 0)) { // clamp to preset override slot
|
||||
loraConfig.channel_num =
|
||||
newRegion->profile->overrideSlot; // use the override slot specified by the region profile
|
||||
channelNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1
|
||||
} else if (newRegion->overrideSlot > 0) { // clamp to explicit override slot
|
||||
loraConfig.channel_num = newRegion->overrideSlot; // use the explicit override slot defined for this region
|
||||
uses_default_frequency_slot = true;
|
||||
} else if (newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH && loraConfig.use_preset) {
|
||||
// clamp to preset name hash
|
||||
loraConfig.channel_num = presetNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1
|
||||
uses_default_frequency_slot = true;
|
||||
} else if (loraConfig.use_preset) { // clamp to preset slot
|
||||
loraConfig.channel_num = presetNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1
|
||||
@@ -1018,6 +1112,8 @@ void RadioInterface::applyModemConfig()
|
||||
// Calculate hash of channel name and preset name to pick a default frequency slot if user has not specified one.
|
||||
// Note that channel_num is actually (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to
|
||||
// (numFreqSlots - 1).
|
||||
const char *channelName = channels.getName(channels.getPrimaryIndex());
|
||||
uint32_t channelNameHashSlot = hash(channelName) % numFreqSlots;
|
||||
uint32_t presetNameHashSlot =
|
||||
hash(DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset)) % numFreqSlots;
|
||||
|
||||
@@ -1034,11 +1130,13 @@ void RadioInterface::applyModemConfig()
|
||||
// (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to (numFreqSlots - 1).
|
||||
// NB: channel_num is also know as frequency slot but it's too late to fix now.
|
||||
if (uses_default_frequency_slot) {
|
||||
// if there's an override slot, use that
|
||||
if (newRegion->profile->overrideSlot != 0) {
|
||||
channel_num = newRegion->profile->overrideSlot - 1;
|
||||
// Handle three override slot cases: explicit slot (>0), preset hash (-1), or channel hash (0)
|
||||
if (newRegion->overrideSlot > 0) {
|
||||
channel_num = newRegion->overrideSlot - 1; // explicit override slot (1-based to 0-based)
|
||||
} else if (newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) {
|
||||
channel_num = presetNameHashSlot; // use preset name hash
|
||||
} else {
|
||||
channel_num = presetNameHashSlot;
|
||||
channel_num = channelNameHashSlot; // use channel name hash (default case)
|
||||
}
|
||||
} else { // use the manually defined one
|
||||
channel_num = loraConfig.channel_num - 1;
|
||||
@@ -1051,7 +1149,6 @@ void RadioInterface::applyModemConfig()
|
||||
|
||||
saveChannelNum(channel_num);
|
||||
saveFreq(freq + loraConfig.frequency_offset);
|
||||
const char *channelName = channels.getName(channels.getPrimaryIndex());
|
||||
|
||||
if (newRegion->wideLora) { // clamp if wide freq range
|
||||
preambleLength = wideLoraPreambleLengthDefault; // 12 is the default for operation above 2GHz
|
||||
@@ -1068,9 +1165,11 @@ void RadioInterface::applyModemConfig()
|
||||
channel_num, power);
|
||||
LOG_INFO("newRegion->freqStart -> newRegion->freqEnd: %f -> %f (%f MHz)", newRegion->freqStart, newRegion->freqEnd,
|
||||
newRegion->freqEnd - newRegion->freqStart);
|
||||
LOG_INFO("numFreqSlots: %d x %.3fkHz", numFreqSlots, bw);
|
||||
if (newRegion->profile->overrideSlot != 0) {
|
||||
LOG_INFO("Using region override slot: %d", newRegion->profile->overrideSlot);
|
||||
LOG_INFO("numFreqSlots: %u x %.3fkHz", numFreqSlots, bw);
|
||||
if (newRegion->overrideSlot > 0) {
|
||||
LOG_INFO("Using region explicit override slot: %d", newRegion->overrideSlot);
|
||||
} else if (newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) {
|
||||
LOG_INFO("Using region preset name hash for slot selection");
|
||||
}
|
||||
LOG_INFO("channel_num: %d", channel_num + 1);
|
||||
LOG_INFO("frequency: %f", getFreq());
|
||||
|
||||
@@ -283,6 +283,12 @@ class RadioInterface
|
||||
*/
|
||||
virtual void saveChannelNum(uint32_t savedChannelNum);
|
||||
|
||||
/**
|
||||
* Get current RSSI reading from the radio.
|
||||
* Returns 0 if not available.
|
||||
*/
|
||||
virtual int16_t getCurrentRSSI() { return 0; }
|
||||
|
||||
private:
|
||||
/**
|
||||
* Convert our modemConfig enum into wf, sf, etc...
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "PortduinoGlue.h"
|
||||
#include "meshUtils.h"
|
||||
#endif
|
||||
|
||||
void LockingArduinoHal::spiBeginTransaction()
|
||||
{
|
||||
spiLock->lock();
|
||||
@@ -28,6 +29,7 @@ void LockingArduinoHal::spiEndTransaction()
|
||||
|
||||
spiLock->unlock();
|
||||
}
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
void LockingArduinoHal::spiTransfer(uint8_t *out, size_t len, uint8_t *in)
|
||||
{
|
||||
@@ -40,6 +42,12 @@ RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE c
|
||||
: NotifiedWorkerThread("RadioIf"), module(hal, cs, irq, rst, busy), iface(_iface)
|
||||
{
|
||||
instance = this;
|
||||
|
||||
// Initialize unused sample slots to a sane default; sample count controls averaging.
|
||||
for (uint8_t i = 0; i < NOISE_FLOOR_SAMPLES; i++) {
|
||||
noiseFloorSamples[i] = NOISE_FLOOR_DEFAULT;
|
||||
}
|
||||
|
||||
#if defined(ARCH_STM32WL) && defined(USE_SX1262)
|
||||
module.setCb_digitalWrite(stm32wl_emulate_digitalWrite);
|
||||
module.setCb_digitalRead(stm32wl_emulate_digitalRead);
|
||||
@@ -246,6 +254,87 @@ bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id)
|
||||
return txQueue.find(from, id);
|
||||
}
|
||||
|
||||
void RadioLibInterface::updateNoiseFloor()
|
||||
{
|
||||
// Only sample from idle receive mode. TX/RX-critical paths must return to radio work quickly.
|
||||
if (!isReceiving || sendingPacket != NULL || isActivelyReceiving() || isIRQPending()) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now = millis();
|
||||
if (now - lastNoiseFloorUpdate < NOISE_FLOOR_UPDATE_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
lastNoiseFloorUpdate = now;
|
||||
|
||||
int16_t rssi = getCurrentRSSI();
|
||||
if (rssi == NOISE_FLOOR_INVALID || rssi >= 0 || rssi < NOISE_FLOOR_VALID_MIN) {
|
||||
LOG_DEBUG("Skipping invalid RSSI reading: %d", rssi);
|
||||
return;
|
||||
}
|
||||
|
||||
noiseFloorSamples[currentSampleIndex] = (int32_t)rssi;
|
||||
currentSampleIndex++;
|
||||
|
||||
if (currentSampleIndex >= NOISE_FLOOR_SAMPLES) {
|
||||
currentSampleIndex = 0;
|
||||
isNoiseFloorBufferFull = true;
|
||||
}
|
||||
|
||||
currentNoiseFloor = getAverageNoiseFloorInternal();
|
||||
|
||||
LOG_DEBUG("Noise floor: %d dBm (samples: %d, latest: %d dBm)", currentNoiseFloor, getNoiseFloorSampleCountInternal(), rssi);
|
||||
}
|
||||
|
||||
uint8_t RadioLibInterface::getNoiseFloorSampleCountInternal() const
|
||||
{
|
||||
return isNoiseFloorBufferFull ? NOISE_FLOOR_SAMPLES : currentSampleIndex;
|
||||
}
|
||||
|
||||
int32_t RadioLibInterface::getAverageNoiseFloorInternal() const
|
||||
{
|
||||
uint8_t sampleCount = getNoiseFloorSampleCountInternal();
|
||||
|
||||
if (sampleCount == 0) {
|
||||
return NOISE_FLOOR_DEFAULT;
|
||||
}
|
||||
|
||||
int32_t sum = 0;
|
||||
for (uint8_t i = 0; i < sampleCount; i++) {
|
||||
sum += noiseFloorSamples[i];
|
||||
}
|
||||
|
||||
return sum / sampleCount;
|
||||
}
|
||||
|
||||
int32_t RadioLibInterface::getAverageNoiseFloor()
|
||||
{
|
||||
return getAverageNoiseFloorInternal();
|
||||
}
|
||||
|
||||
int32_t RadioLibInterface::getNoiseFloor()
|
||||
{
|
||||
return currentNoiseFloor;
|
||||
}
|
||||
|
||||
bool RadioLibInterface::hasNoiseFloorSamples()
|
||||
{
|
||||
return getNoiseFloorSampleCountInternal() > 0;
|
||||
}
|
||||
|
||||
uint8_t RadioLibInterface::getNoiseFloorSampleCount()
|
||||
{
|
||||
return getNoiseFloorSampleCountInternal();
|
||||
}
|
||||
|
||||
void RadioLibInterface::resetNoiseFloor()
|
||||
{
|
||||
currentSampleIndex = 0;
|
||||
isNoiseFloorBufferFull = false;
|
||||
currentNoiseFloor = NOISE_FLOOR_DEFAULT;
|
||||
LOG_INFO("Noise floor reset - rolling window collection will restart");
|
||||
}
|
||||
|
||||
bool RadioLibInterface::randomBytes(uint8_t *buffer, size_t length)
|
||||
{
|
||||
if (!buffer || length == 0 || !iface) {
|
||||
@@ -273,6 +362,7 @@ currently active.
|
||||
*/
|
||||
void RadioLibInterface::onNotify(uint32_t notification)
|
||||
{
|
||||
|
||||
switch (notification) {
|
||||
case ISR_TX:
|
||||
handleTransmitInterrupt();
|
||||
@@ -404,11 +494,6 @@ bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a packet that is eligible for replacement from the TX queue
|
||||
*/
|
||||
// void RadioLibInterface::removePending
|
||||
|
||||
void RadioLibInterface::handleTransmitInterrupt()
|
||||
{
|
||||
// This can be null if we forced the device to enter standby mode. In that case
|
||||
|
||||
@@ -99,6 +99,26 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
/// are _trying_ to receive a packet currently (note - we might just be waiting for one)
|
||||
bool isReceiving = false;
|
||||
|
||||
protected:
|
||||
// Noise floor tracking - rolling window of samples.
|
||||
static const uint8_t NOISE_FLOOR_SAMPLES = 20;
|
||||
static const int32_t NOISE_FLOOR_DEFAULT = -120;
|
||||
static const int32_t NOISE_FLOOR_VALID_MIN = -127;
|
||||
static const int32_t NOISE_FLOOR_INVALID = -128;
|
||||
int32_t noiseFloorSamples[NOISE_FLOOR_SAMPLES];
|
||||
uint8_t currentSampleIndex = 0;
|
||||
bool isNoiseFloorBufferFull = false;
|
||||
uint32_t lastNoiseFloorUpdate = 0;
|
||||
static const uint32_t NOISE_FLOOR_UPDATE_INTERVAL_MS = 5000;
|
||||
int32_t currentNoiseFloor = NOISE_FLOOR_DEFAULT;
|
||||
|
||||
/**
|
||||
* Pure virtual hook for derived radio interfaces to provide instantaneous RSSI.
|
||||
* Implementations should return dBm, or an invalid value that updateNoiseFloor()
|
||||
* can reject.
|
||||
*/
|
||||
virtual int16_t getCurrentRSSI() = 0;
|
||||
|
||||
public:
|
||||
/** Our ISR code currently needs this to find our active instance
|
||||
*/
|
||||
@@ -111,6 +131,17 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
instance = nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current calculated noise floor in dBm
|
||||
* Returns -120 dBm if not yet calibrated
|
||||
*/
|
||||
int32_t getNoiseFloor();
|
||||
|
||||
/**
|
||||
* Calculate the average noise floor from collected samples
|
||||
*/
|
||||
int32_t getAverageNoiseFloor();
|
||||
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
@@ -179,6 +210,28 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */
|
||||
virtual bool findInTxQueue(NodeNum from, PacketId id) override;
|
||||
|
||||
/**
|
||||
* Update the noise floor measurement by sampling RSSI from a slow path.
|
||||
* This should not be called from radio interrupt or TX/RX critical paths.
|
||||
*/
|
||||
void updateNoiseFloor();
|
||||
|
||||
/**
|
||||
* Check if we have collected any noise floor samples
|
||||
*/
|
||||
bool hasNoiseFloorSamples();
|
||||
|
||||
/**
|
||||
* Get the number of samples in the rolling window
|
||||
*/
|
||||
uint8_t getNoiseFloorSampleCount();
|
||||
|
||||
/**
|
||||
* Reset the noise floor calibration
|
||||
* Will automatically restart collection
|
||||
*/
|
||||
void resetNoiseFloor();
|
||||
|
||||
/**
|
||||
* Request randomness sourced from the LoRa modem, if supported by the active RadioLib interface.
|
||||
* @return true if len bytes were produced, false otherwise.
|
||||
@@ -186,6 +239,9 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
bool randomBytes(uint8_t *buffer, size_t length);
|
||||
|
||||
private:
|
||||
uint8_t getNoiseFloorSampleCountInternal() const;
|
||||
int32_t getAverageNoiseFloorInternal() const;
|
||||
|
||||
/** if we have something waiting to send, start a short (random) timer so we can come check for collision before actually
|
||||
* doing the transmit */
|
||||
void setTransmitDelay();
|
||||
|
||||
+41
-17
@@ -15,6 +15,9 @@
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
#include "modules/TrafficManagementModule.h"
|
||||
#endif
|
||||
#if HAS_VARIABLE_HOPS
|
||||
#include "modules/HopScalingModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_MQTT
|
||||
#include "mqtt/MQTT.h"
|
||||
#endif
|
||||
@@ -22,8 +25,6 @@
|
||||
#if ARCH_PORTDUINO
|
||||
#include "Throttle.h"
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
#endif
|
||||
#if ENABLE_JSON_LOGGING || ARCH_PORTDUINO
|
||||
#include "serialization/MeshPacketSerializer.h"
|
||||
#endif
|
||||
|
||||
@@ -318,10 +319,11 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
|
||||
} // should have already been handled by sendLocal
|
||||
|
||||
// Abort sending if we are violating the duty cycle
|
||||
if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) {
|
||||
float effectiveDutyCycle = getEffectiveDutyCycle();
|
||||
if (!config.lora.override_duty_cycle && effectiveDutyCycle < 100) {
|
||||
float hourlyTxPercent = airTime->utilizationTXPercent();
|
||||
if (hourlyTxPercent > myRegion->dutyCycle) {
|
||||
uint8_t silentMinutes = airTime->getSilentMinutes(hourlyTxPercent, myRegion->dutyCycle);
|
||||
if (hourlyTxPercent > effectiveDutyCycle) {
|
||||
uint8_t silentMinutes = airTime->getSilentMinutes(hourlyTxPercent, effectiveDutyCycle);
|
||||
|
||||
LOG_WARN("Duty cycle limit exceeded. Aborting send for now, you can send again in %d mins", silentMinutes);
|
||||
|
||||
@@ -356,6 +358,30 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
|
||||
p->from = getFrom(p);
|
||||
|
||||
p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // set the relayer to us
|
||||
|
||||
#if HAS_VARIABLE_HOPS
|
||||
// Apply HopScaling hop recommendation to routine outgoing broadcasts
|
||||
if (isFromUs(p) && isBroadcast(p->to) && hopScalingModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
switch (p->decoded.portnum) {
|
||||
case meshtastic_PortNum_POSITION_APP:
|
||||
case meshtastic_PortNum_TELEMETRY_APP:
|
||||
case meshtastic_PortNum_NODEINFO_APP:
|
||||
case meshtastic_PortNum_NEIGHBORINFO_APP: {
|
||||
uint8_t variableHopLimit = hopScalingModule->getLastRequiredHop();
|
||||
|
||||
// Never exceed user-configured hop_limit
|
||||
if (variableHopLimit < p->hop_limit) {
|
||||
LOG_DEBUG("[HOPSCALE] hop_limit %u -> %u for portnum %u", p->hop_limit, variableHopLimit, p->decoded.portnum);
|
||||
p->hop_limit = variableHopLimit;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// If we are the original transmitter, set the hop limit with which we start
|
||||
if (isFromUs(p))
|
||||
p->hop_start = p->hop_limit;
|
||||
@@ -368,10 +394,14 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
|
||||
}
|
||||
|
||||
fixPriority(p); // Before encryption, fix the priority if it's unset
|
||||
if (!applyPositionPrecisionForChannel(*p, p->channel)) {
|
||||
LOG_ERROR("Dropping malformed position packet before send");
|
||||
packetPool.release(p);
|
||||
return meshtastic_Routing_Error_BAD_REQUEST;
|
||||
// Position precision is an originator-only privacy policy. Relays keep
|
||||
// p->from as the original sender, so do not rewrite their POSITION_APP payload.
|
||||
if (isFromUs(p)) {
|
||||
if (!applyPositionPrecisionForChannel(*p, p->channel)) {
|
||||
LOG_ERROR("Dropping malformed position packet before send");
|
||||
packetPool.release(p);
|
||||
return meshtastic_Routing_Error_BAD_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
// If the packet is not yet encrypted, do so now
|
||||
@@ -550,9 +580,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
} */
|
||||
|
||||
printPacket("decoded message", p);
|
||||
#if ENABLE_JSON_LOGGING
|
||||
LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str());
|
||||
#elif ARCH_PORTDUINO
|
||||
#if ARCH_PORTDUINO
|
||||
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
|
||||
LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str());
|
||||
} else if (portduino_config.JSONFilename != "") {
|
||||
@@ -847,11 +875,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
|
||||
void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
||||
{
|
||||
#if ENABLE_JSON_LOGGING
|
||||
// Even ignored packets get logged in the trace
|
||||
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
|
||||
LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str());
|
||||
#elif ARCH_PORTDUINO
|
||||
#if ARCH_PORTDUINO
|
||||
// Even ignored packets get logged in the trace
|
||||
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
|
||||
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
|
||||
|
||||
@@ -52,8 +52,8 @@ template <typename T> bool SX126xInterface<T>::init()
|
||||
|
||||
#ifdef SX126X_POWER_EN // Perhaps add RADIOLIB_NC check, and beforehand define as such if it is undefined, but it is not commonly
|
||||
// used and not part of the 'default' set of pin definitions.
|
||||
digitalWrite(SX126X_POWER_EN, HIGH);
|
||||
pinMode(SX126X_POWER_EN, OUTPUT);
|
||||
digitalWrite(SX126X_POWER_EN, HIGH);
|
||||
#endif
|
||||
|
||||
#if HAS_LORA_FEM
|
||||
@@ -65,8 +65,8 @@ template <typename T> bool SX126xInterface<T>::init()
|
||||
#endif
|
||||
|
||||
#ifdef RF95_FAN_EN
|
||||
digitalWrite(RF95_FAN_EN, HIGH);
|
||||
pinMode(RF95_FAN_EN, OUTPUT);
|
||||
digitalWrite(RF95_FAN_EN, HIGH);
|
||||
#endif
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
@@ -265,6 +265,12 @@ template <typename T> bool SX126xInterface<T>::reconfigure()
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T> int16_t SX126xInterface<T>::getCurrentRSSI()
|
||||
{
|
||||
float rssi = lora.getRSSI(false);
|
||||
return (int16_t)round(rssi);
|
||||
}
|
||||
|
||||
template <typename T> void SX126xInterface<T>::disableInterrupt()
|
||||
{
|
||||
lora.clearDio1Action();
|
||||
|
||||
@@ -41,6 +41,8 @@ template <class T> class SX126xInterface : public RadioLibInterface
|
||||
*/
|
||||
T lora;
|
||||
|
||||
int16_t getCurrentRSSI() override;
|
||||
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
|
||||
@@ -325,4 +325,10 @@ template <typename T> bool SX128xInterface<T>::sleep()
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T> int16_t SX128xInterface<T>::getCurrentRSSI()
|
||||
{
|
||||
float rssi = lora.getRSSI(false);
|
||||
return (int16_t)round(rssi);
|
||||
}
|
||||
#endif
|
||||
@@ -35,6 +35,8 @@ template <class T> class SX128xInterface : public RadioLibInterface
|
||||
*/
|
||||
T lora;
|
||||
|
||||
int16_t getCurrentRSSI() override;
|
||||
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
#include "ServerAPI.h"
|
||||
#if !defined(USE_WS5500) && !defined(USE_CH390D)
|
||||
#if defined(USE_ARDUINO_ETHERNET)
|
||||
#include <Ethernet.h>
|
||||
#else
|
||||
#include <RAK13800_W5100S.h>
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Provides both debug printing and, if the client starts sending protobufs to us, switches to send/receive protobufs
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,347 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Siara Logics (cc)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @author Arundale Ramanathan
|
||||
*
|
||||
* Port for Particle (particle.io) / Aruino - Jonathan Greenblatt
|
||||
*
|
||||
* This file describes each function of the Unishox2 API \n
|
||||
* For finding out how this API can be used in your program, \n
|
||||
* please see test_unishox2.c.
|
||||
*/
|
||||
|
||||
#ifndef unishox2
|
||||
#define unishox2
|
||||
|
||||
#define UNISHOX_VERSION "2.0" ///< Unicode spec version
|
||||
|
||||
/**
|
||||
* Macro switch to enable/disable output buffer length parameter in low level api \n
|
||||
* Disabled by default \n
|
||||
* When this macro is defined, the all the API functions \n
|
||||
* except the simple API functions accept an additional parameter olen \n
|
||||
* that enables the developer to pass the size of the output buffer provided \n
|
||||
* so that the api function may not write beyond that length. \n
|
||||
* This can be disabled if the developer knows that the buffer provided is sufficient enough \n
|
||||
* so no additional parameter is passed and the program is faster since additional check \n
|
||||
* for output length is not performed at each step \n
|
||||
* The simple api, i.e. unishox2_(de)compress_simple will always omit the buffer length
|
||||
*/
|
||||
#ifndef UNISHOX_API_WITH_OUTPUT_LEN
|
||||
#define UNISHOX_API_WITH_OUTPUT_LEN 1
|
||||
#endif
|
||||
|
||||
/// Upto 8 bits of initial magic bit sequence can be included. Bit count can be specified with UNISHOX_MAGIC_BIT_LEN
|
||||
#ifndef UNISHOX_MAGIC_BITS
|
||||
#define UNISHOX_MAGIC_BITS 0xFF
|
||||
#endif
|
||||
|
||||
/// Desired length of Magic bits defined by UNISHOX_MAGIC_BITS
|
||||
#ifdef UNISHOX_MAGIC_BIT_LEN
|
||||
#if UNISHOX_MAGIC_BIT_LEN < 0 || 9 <= UNISHOX_MAGIC_BIT_LEN
|
||||
#error "UNISHOX_MAGIC_BIT_LEN need between [0, 8)"
|
||||
#endif
|
||||
#else
|
||||
#define UNISHOX_MAGIC_BIT_LEN 1
|
||||
#endif
|
||||
|
||||
// enum {USX_ALPHA = 0, USX_SYM, USX_NUM, USX_DICT, USX_DELTA};
|
||||
|
||||
/// Default Horizontal codes. When composition of text is know beforehand, the other hcodes in this section can be used to achieve
|
||||
/// more compression.
|
||||
#define USX_HCODES_DFLT \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
0x00, 0x40, 0x80, 0xC0, 0xE0 \
|
||||
}
|
||||
/// Length of each default hcode
|
||||
#define USX_HCODE_LENS_DFLT \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
2, 2, 2, 3, 3 \
|
||||
}
|
||||
|
||||
/// Horizontal codes preset for English Alphabet content only
|
||||
#define USX_HCODES_ALPHA_ONLY \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
0x00, 0x00, 0x00, 0x00, 0x00 \
|
||||
}
|
||||
/// Length of each Alpha only hcode
|
||||
#define USX_HCODE_LENS_ALPHA_ONLY \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
0, 0, 0, 0, 0 \
|
||||
}
|
||||
|
||||
/// Horizontal codes preset for Alpha Numeric content only
|
||||
#define USX_HCODES_ALPHA_NUM_ONLY \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
0x00, 0x00, 0x80, 0x00, 0x00 \
|
||||
}
|
||||
/// Length of each Alpha numeric hcode
|
||||
#define USX_HCODE_LENS_ALPHA_NUM_ONLY \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
1, 0, 1, 0, 0 \
|
||||
}
|
||||
|
||||
/// Horizontal codes preset for Alpha Numeric and Symbol content only
|
||||
#define USX_HCODES_ALPHA_NUM_SYM_ONLY \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
0x00, 0x80, 0xC0, 0x00, 0x00 \
|
||||
}
|
||||
/// Length of each Alpha numeric and symbol hcodes
|
||||
#define USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
1, 2, 2, 0, 0 \
|
||||
}
|
||||
|
||||
/// Horizontal codes preset favouring Alphabet content
|
||||
#define USX_HCODES_FAVOR_ALPHA \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
0x00, 0x80, 0xA0, 0xC0, 0xE0 \
|
||||
}
|
||||
/// Length of each hcode favouring Alpha content
|
||||
#define USX_HCODE_LENS_FAVOR_ALPHA \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
1, 3, 3, 3, 3 \
|
||||
}
|
||||
|
||||
/// Horizontal codes preset favouring repeating sequences
|
||||
#define USX_HCODES_FAVOR_DICT \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
0x00, 0x40, 0xC0, 0x80, 0xE0 \
|
||||
}
|
||||
/// Length of each hcode favouring repeating sequences
|
||||
#define USX_HCODE_LENS_FAVOR_DICT \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
2, 2, 3, 2, 3 \
|
||||
}
|
||||
|
||||
/// Horizontal codes preset favouring symbols
|
||||
#define USX_HCODES_FAVOR_SYM \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
0x80, 0x00, 0xA0, 0xC0, 0xE0 \
|
||||
}
|
||||
/// Length of each hcode favouring symbols
|
||||
#define USX_HCODE_LENS_FAVOR_SYM \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
3, 1, 3, 3, 3 \
|
||||
}
|
||||
|
||||
// #define USX_HCODES_FAVOR_UMLAUT {0x00, 0x40, 0xE0, 0xC0, 0x80}
|
||||
// #define USX_HCODE_LENS_FAVOR_UMLAUT {2, 2, 3, 3, 2}
|
||||
|
||||
/// Horizontal codes preset favouring umlaut letters
|
||||
#define USX_HCODES_FAVOR_UMLAUT \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
0x80, 0xA0, 0xC0, 0xE0, 0x00 \
|
||||
}
|
||||
/// Length of each hcode favouring umlaut letters
|
||||
#define USX_HCODE_LENS_FAVOR_UMLAUT \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
3, 3, 3, 3, 1 \
|
||||
}
|
||||
|
||||
/// Horizontal codes preset for no repeating sequences
|
||||
#define USX_HCODES_NO_DICT \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
0x00, 0x40, 0x80, 0x00, 0xC0 \
|
||||
}
|
||||
/// Length of each hcode for no repeating sequences
|
||||
#define USX_HCODE_LENS_NO_DICT \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
2, 2, 2, 0, 2 \
|
||||
}
|
||||
|
||||
/// Horizontal codes preset for no Unicode characters
|
||||
#define USX_HCODES_NO_UNI \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
0x00, 0x40, 0x80, 0xC0, 0x00 \
|
||||
}
|
||||
/// Length of each hcode for no Unicode characters
|
||||
#define USX_HCODE_LENS_NO_UNI \
|
||||
(const unsigned char[]) \
|
||||
{ \
|
||||
2, 2, 2, 2, 0 \
|
||||
}
|
||||
|
||||
extern const char *USX_FREQ_SEQ_DFLT[];
|
||||
extern const char *USX_FREQ_SEQ_TXT[];
|
||||
extern const char *USX_FREQ_SEQ_URL[];
|
||||
extern const char *USX_FREQ_SEQ_JSON[];
|
||||
extern const char *USX_FREQ_SEQ_HTML[];
|
||||
extern const char *USX_FREQ_SEQ_XML[];
|
||||
extern const char *USX_TEMPLATES[];
|
||||
|
||||
/// Default preset parameter set. When composition of text is know beforehand, the other parameter sets in this section can be
|
||||
/// used to achieve more compression.
|
||||
#define USX_PSET_DFLT USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
|
||||
/// Preset parameter set for English Alphabet only content
|
||||
#define USX_PSET_ALPHA_ONLY USX_HCODES_ALPHA_ONLY, USX_HCODE_LENS_ALPHA_ONLY, USX_FREQ_SEQ_TXT, USX_TEMPLATES
|
||||
/// Preset parameter set for Alpha numeric content
|
||||
#define USX_PSET_ALPHA_NUM_ONLY USX_HCODES_ALPHA_NUM_ONLY, USX_HCODE_LENS_ALPHA_NUM_ONLY, USX_FREQ_SEQ_TXT, USX_TEMPLATES
|
||||
/// Preset parameter set for Alpha numeric and symbol content
|
||||
#define USX_PSET_ALPHA_NUM_SYM_ONLY \
|
||||
USX_HCODES_ALPHA_NUM_SYM_ONLY, USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
|
||||
/// Preset parameter set for Alpha numeric symbol content having predominantly text
|
||||
#define USX_PSET_ALPHA_NUM_SYM_ONLY_TXT \
|
||||
USX_HCODES_ALPHA_NUM_SYM_ONLY, USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
|
||||
/// Preset parameter set favouring Alphabet content
|
||||
#define USX_PSET_FAVOR_ALPHA USX_HCODES_FAVOR_ALPHA, USX_HCODE_LENS_FAVOR_ALPHA, USX_FREQ_SEQ_TXT, USX_TEMPLATES
|
||||
/// Preset parameter set favouring repeating sequences
|
||||
#define USX_PSET_FAVOR_DICT USX_HCODES_FAVOR_DICT, USX_HCODE_LENS_FAVOR_DICT, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
|
||||
/// Preset parameter set favouring symbols
|
||||
#define USX_PSET_FAVOR_SYM USX_HCODES_FAVOR_SYM, USX_HCODE_LENS_FAVOR_SYM, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
|
||||
/// Preset parameter set favouring unlaut letters
|
||||
#define USX_PSET_FAVOR_UMLAUT USX_HCODES_FAVOR_UMLAUT, USX_HCODE_LENS_FAVOR_UMLAUT, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
|
||||
/// Preset parameter set for when there are no repeating sequences
|
||||
#define USX_PSET_NO_DICT USX_HCODES_NO_DICT, USX_HCODE_LENS_NO_DICT, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
|
||||
/// Preset parameter set for when there are no unicode symbols
|
||||
#define USX_PSET_NO_UNI USX_HCODES_NO_UNI, USX_HCODE_LENS_NO_UNI, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
|
||||
/// Preset parameter set for when there are no unicode symbols favouring text
|
||||
#define USX_PSET_NO_UNI_FAVOR_TEXT USX_HCODES_NO_UNI, USX_HCODE_LENS_NO_UNI, USX_FREQ_SEQ_TXT, USX_TEMPLATES
|
||||
/// Preset parameter set favouring URL content
|
||||
#define USX_PSET_URL USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_URL, USX_TEMPLATES
|
||||
/// Preset parameter set favouring JSON content
|
||||
#define USX_PSET_JSON USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_JSON, USX_TEMPLATES
|
||||
/// Preset parameter set favouring JSON content having no Unicode symbols
|
||||
#define USX_PSET_JSON_NO_UNI USX_HCODES_NO_UNI, USX_HCODE_LENS_NO_UNI, USX_FREQ_SEQ_JSON, USX_TEMPLATES
|
||||
/// Preset parameter set favouring XML content
|
||||
#define USX_PSET_XML USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_XML, USX_TEMPLATES
|
||||
/// Preset parameter set favouring HTML content
|
||||
#define USX_PSET_HTML USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_HTML, USX_TEMPLATES
|
||||
|
||||
/**
|
||||
* This structure is used when a string array needs to be compressed.
|
||||
* This is passed as a parameter to the unishox2_decompress_lines() function
|
||||
*/
|
||||
struct us_lnk_lst {
|
||||
char *data;
|
||||
struct us_lnk_lst *previous;
|
||||
};
|
||||
|
||||
/**
|
||||
* This macro is for internal use, but builds upon the macro UNISHOX_API_WITH_OUTPUT_LEN
|
||||
* When the macro UNISHOX_API_WITH_OUTPUT_LEN is defined, the all the API functions
|
||||
* except the simple API functions accept an additional parameter olen
|
||||
* that enables the developer to pass the size of the output buffer provided
|
||||
* so that the api function may not write beyond that length.
|
||||
* This can be disabled if the developer knows that the buffer provided is sufficient enough
|
||||
* so no additional parameter is passed and the program is faster since additional check
|
||||
* for output length is not performed at each step
|
||||
*/
|
||||
#if defined(UNISHOX_API_WITH_OUTPUT_LEN) && UNISHOX_API_WITH_OUTPUT_LEN != 0
|
||||
#define UNISHOX_API_OUT_AND_LEN(out, olen) out, olen
|
||||
#else
|
||||
#define UNISHOX_API_OUT_AND_LEN(out, olen) out
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Simple API for compressing a string
|
||||
* @param[in] in Input ASCII / UTF-8 string
|
||||
* @param[in] len length in bytes
|
||||
* @param[out] out output buffer - should be large enough to hold compressed output
|
||||
*/
|
||||
extern int unishox2_compress_simple(const char *in, int len, char *out);
|
||||
/**
|
||||
* Simple API for decompressing a string
|
||||
* @param[in] in Input compressed bytes (output of unishox2_compress functions)
|
||||
* @param[in] len length of 'in' in bytes
|
||||
* @param[out] out output buffer for ASCII / UTF-8 string - should be large enough
|
||||
*/
|
||||
extern int unishox2_decompress_simple(const char *in, int len, char *out);
|
||||
/**
|
||||
* Comprehensive API for compressing a string
|
||||
*
|
||||
* Presets are available for the last four parameters so they can be passed as single parameter. \n
|
||||
* See USX_PSET_* macros. Example call: \n
|
||||
* unishox2_compress(in, len, out, olen, USX_PSET_ALPHA_ONLY);
|
||||
*
|
||||
* @param[in] in Input ASCII / UTF-8 string
|
||||
* @param[in] len length in bytes
|
||||
* @param[out] out output buffer - should be large enough to hold compressed output
|
||||
* @param[in] olen length of 'out' buffer in bytes. Can be omitted if sufficient buffer is provided
|
||||
* @param[in] usx_hcodes Horizontal codes (array of bytes). See macro section for samples.
|
||||
* @param[in] usx_hcode_lens Length of each element in usx_hcodes array
|
||||
* @param[in] usx_freq_seq Frequently occurring sequences. See USX_FREQ_SEQ_* macros for samples
|
||||
* @param[in] usx_templates Templates of frequently occurring patterns. See USX_TEMPLATES macro.
|
||||
*/
|
||||
extern int unishox2_compress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen),
|
||||
const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], const char *usx_freq_seq[],
|
||||
const char *usx_templates[]);
|
||||
/**
|
||||
* Comprehensive API for de-compressing a string
|
||||
*
|
||||
* Presets are available for the last four parameters so they can be passed as single parameter. \n
|
||||
* See USX_PSET_* macros. Example call: \n
|
||||
* unishox2_decompress(in, len, out, olen, USX_PSET_ALPHA_ONLY);
|
||||
*
|
||||
* @param[in] in Input compressed bytes (output of unishox2_compress functions)
|
||||
* @param[in] len length of 'in' in bytes
|
||||
* @param[out] out output buffer - should be large enough to hold de-compressed output
|
||||
* @param[in] olen length of 'out' buffer in bytes. Can be omitted if sufficient buffer is provided
|
||||
* @param[in] usx_hcodes Horizontal codes (array of bytes). See macro section for samples.
|
||||
* @param[in] usx_hcode_lens Length of each element in usx_hcodes array
|
||||
* @param[in] usx_freq_seq Frequently occurring sequences. See USX_FREQ_SEQ_* macros for samples
|
||||
* @param[in] usx_templates Templates of frequently occurring patterns. See USX_TEMPLATES macro.
|
||||
*/
|
||||
extern int unishox2_decompress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen),
|
||||
const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], const char *usx_freq_seq[],
|
||||
const char *usx_templates[]);
|
||||
/**
|
||||
* More Comprehensive API for compressing array of strings
|
||||
*
|
||||
* See unishox2_compress() function for parameter definitions. \n
|
||||
* This function takes an additional parameter, i.e. 'prev_lines' - the usx_lnk_lst structure \n
|
||||
* See -g parameter in test_unishox2.c to find out how this can be used. \n
|
||||
* This function is used when an array of strings need to be compressed \n
|
||||
* and stored in a compressed array of bytes for use as a constant in other programs \n
|
||||
* where each element of the array can be decompressed and used at runtime.
|
||||
*/
|
||||
extern int unishox2_compress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen),
|
||||
const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[],
|
||||
const char *usx_freq_seq[], const char *usx_templates[], struct us_lnk_lst *prev_lines);
|
||||
/**
|
||||
* More Comprehensive API for de-compressing array of strings \n
|
||||
* This function is not be used in conjuction with unishox2_compress_lines()
|
||||
*
|
||||
* See unishox2_decompress() function for parameter definitions. \n
|
||||
* Typically an array is compressed using unishox2_compress_lines() and \n
|
||||
* a header (.h) file is generated using the resultant compressed array. \n
|
||||
* This header file can be used in another program with another decompress \n
|
||||
* routine which takes this compressed array as parameter and index to be \n
|
||||
* decompressed.
|
||||
*/
|
||||
extern int unishox2_decompress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen),
|
||||
const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[],
|
||||
const char *usx_freq_seq[], const char *usx_templates[], struct us_lnk_lst *prev_lines);
|
||||
|
||||
#endif
|
||||
@@ -6,7 +6,13 @@
|
||||
#include "main.h"
|
||||
#include "mesh/api/ethServerAPI.h"
|
||||
#include "target_specific.h"
|
||||
#ifdef USE_ARDUINO_ETHERNET
|
||||
#include <Ethernet.h> // arduino-libraries/Ethernet — supports W5100/W5200/W5500
|
||||
// Shorter DHCP timeout so LoRa startup isn't blocked when no DHCP server is present.
|
||||
#define ETH_DHCP_TIMEOUT_MS 10000
|
||||
#else
|
||||
#include <RAK13800_W5100S.h>
|
||||
#endif
|
||||
#include <SPI.h>
|
||||
|
||||
#if HAS_NETWORKING && !defined(USE_WS5500) && !defined(USE_CH390D)
|
||||
@@ -69,6 +75,13 @@ static int32_t reconnectETH()
|
||||
delay(100);
|
||||
#endif
|
||||
|
||||
#ifdef USE_ARDUINO_ETHERNET // Re-configure SPI0 for the W5500 module
|
||||
SPI.setRX(ETH_SPI0_MISO);
|
||||
SPI.setSCK(ETH_SPI0_SCK);
|
||||
SPI.setTX(ETH_SPI0_MOSI);
|
||||
SPI.begin();
|
||||
Ethernet.init(PIN_ETHERNET_SS);
|
||||
#else
|
||||
#ifdef RAK11310
|
||||
ETH_SPI_PORT.setSCK(PIN_SPI0_SCK);
|
||||
ETH_SPI_PORT.setTX(PIN_SPI0_MOSI);
|
||||
@@ -76,10 +89,15 @@ static int32_t reconnectETH()
|
||||
ETH_SPI_PORT.begin();
|
||||
#endif
|
||||
Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS);
|
||||
#endif
|
||||
|
||||
int status = 0;
|
||||
if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_DHCP) {
|
||||
#ifdef ETH_DHCP_TIMEOUT_MS
|
||||
status = Ethernet.begin(expectedMac, ETH_DHCP_TIMEOUT_MS);
|
||||
#else
|
||||
status = Ethernet.begin(expectedMac);
|
||||
#endif
|
||||
} else if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC) {
|
||||
Ethernet.begin(expectedMac, config.network.ipv4_config.ip, config.network.ipv4_config.dns,
|
||||
config.network.ipv4_config.gateway, config.network.ipv4_config.subnet);
|
||||
@@ -182,6 +200,13 @@ bool initEthernet()
|
||||
digitalWrite(PIN_ETHERNET_RESET, HIGH); // Reset Time.
|
||||
#endif
|
||||
|
||||
#ifdef USE_ARDUINO_ETHERNET // Configure SPI0 for the W5500 module
|
||||
SPI.setRX(ETH_SPI0_MISO);
|
||||
SPI.setSCK(ETH_SPI0_SCK);
|
||||
SPI.setTX(ETH_SPI0_MOSI);
|
||||
SPI.begin();
|
||||
Ethernet.init(PIN_ETHERNET_SS);
|
||||
#else
|
||||
#ifdef RAK11310 // Initialize the SPI port
|
||||
ETH_SPI_PORT.setSCK(PIN_SPI0_SCK);
|
||||
ETH_SPI_PORT.setTX(PIN_SPI0_MOSI);
|
||||
@@ -189,6 +214,7 @@ bool initEthernet()
|
||||
ETH_SPI_PORT.begin();
|
||||
#endif
|
||||
Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS);
|
||||
#endif
|
||||
|
||||
uint8_t mac[6];
|
||||
|
||||
@@ -201,7 +227,11 @@ bool initEthernet()
|
||||
|
||||
if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_DHCP) {
|
||||
LOG_INFO("Start Ethernet DHCP");
|
||||
#ifdef ETH_DHCP_TIMEOUT_MS
|
||||
status = Ethernet.begin(mac, ETH_DHCP_TIMEOUT_MS);
|
||||
#else
|
||||
status = Ethernet.begin(mac);
|
||||
#endif
|
||||
} else if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC) {
|
||||
LOG_INFO("Start Ethernet Static");
|
||||
Ethernet.begin(mac, config.network.ipv4_config.ip, config.network.ipv4_config.dns, config.network.ipv4_config.gateway,
|
||||
|
||||
@@ -198,6 +198,28 @@ typedef struct _meshtastic_LockdownAuth {
|
||||
way to reset the session clock is a reboot, which costs a boot
|
||||
from the on-flash, HMAC-bound counter. */
|
||||
uint32_t max_session_seconds;
|
||||
/* Disable lockdown mode. Requires a valid passphrase in the same
|
||||
message (the device must prove the operator owns it before
|
||||
reverting at-rest encryption). On success the firmware decrypts
|
||||
every stored config / channel / nodedb file back to plaintext,
|
||||
removes the wrapped DEK, unlock token, monotonic-counter, and
|
||||
backoff files, and reboots out of lockdown.
|
||||
|
||||
This is the inverse of the provision/unlock path: it is how the
|
||||
client app's "lockdown mode" toggle returns a device to normal
|
||||
operation.
|
||||
|
||||
NOT reversed by this operation: APPROTECT. Once the debug port
|
||||
lockout has been burned (on silicon where it is effective) it is
|
||||
permanent — disabling lockdown decrypts your data and removes the
|
||||
access gates, but the SWD/JTAG port stays locked for the life of
|
||||
the device (recoverable only via a full chip erase over a debug
|
||||
probe, which destroys all data). Clients should make this
|
||||
irreversibility clear at the moment lockdown is first enabled.
|
||||
|
||||
When true the passphrase field is still required; boots_remaining,
|
||||
valid_until_epoch, max_session_seconds, and lock_now are ignored. */
|
||||
bool disable;
|
||||
} meshtastic_LockdownAuth;
|
||||
|
||||
/* Parameters for setting up Meshtastic for ameteur radio usage */
|
||||
@@ -521,7 +543,7 @@ extern "C" {
|
||||
#define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}}
|
||||
#define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0}
|
||||
#define meshtastic_AdminMessage_OTAEvent_init_default {_meshtastic_OTAMode_MIN, {0, {0}}}
|
||||
#define meshtastic_LockdownAuth_init_default {{0, {0}}, 0, 0, 0, 0}
|
||||
#define meshtastic_LockdownAuth_init_default {{0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_HamParameters_init_default {"", 0, 0, ""}
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
|
||||
#define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0, 0}
|
||||
@@ -534,7 +556,7 @@ extern "C" {
|
||||
#define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}}
|
||||
#define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0}
|
||||
#define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {0, {0}}}
|
||||
#define meshtastic_LockdownAuth_init_zero {{0, {0}}, 0, 0, 0, 0}
|
||||
#define meshtastic_LockdownAuth_init_zero {{0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_HamParameters_init_zero {"", 0, 0, ""}
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
|
||||
#define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0, 0}
|
||||
@@ -557,6 +579,7 @@ extern "C" {
|
||||
#define meshtastic_LockdownAuth_valid_until_epoch_tag 3
|
||||
#define meshtastic_LockdownAuth_lock_now_tag 4
|
||||
#define meshtastic_LockdownAuth_max_session_seconds_tag 5
|
||||
#define meshtastic_LockdownAuth_disable_tag 6
|
||||
#define meshtastic_HamParameters_call_sign_tag 1
|
||||
#define meshtastic_HamParameters_tx_power_tag 2
|
||||
#define meshtastic_HamParameters_frequency_tag 3
|
||||
@@ -754,7 +777,8 @@ X(a, STATIC, SINGULAR, BYTES, passphrase, 1) \
|
||||
X(a, STATIC, SINGULAR, UINT32, boots_remaining, 2) \
|
||||
X(a, STATIC, SINGULAR, UINT32, valid_until_epoch, 3) \
|
||||
X(a, STATIC, SINGULAR, BOOL, lock_now, 4) \
|
||||
X(a, STATIC, SINGULAR, UINT32, max_session_seconds, 5)
|
||||
X(a, STATIC, SINGULAR, UINT32, max_session_seconds, 5) \
|
||||
X(a, STATIC, SINGULAR, BOOL, disable, 6)
|
||||
#define meshtastic_LockdownAuth_CALLBACK NULL
|
||||
#define meshtastic_LockdownAuth_DEFAULT NULL
|
||||
|
||||
@@ -869,7 +893,7 @@ extern const pb_msgdesc_t meshtastic_SHTXX_config_msg;
|
||||
#define meshtastic_AdminMessage_size 511
|
||||
#define meshtastic_HamParameters_size 31
|
||||
#define meshtastic_KeyVerificationAdmin_size 25
|
||||
#define meshtastic_LockdownAuth_size 54
|
||||
#define meshtastic_LockdownAuth_size 56
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_size 496
|
||||
#define meshtastic_SCD30_config_size 27
|
||||
#define meshtastic_SCD4X_config_size 29
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user