mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-15 23:32:34 +00:00
Compare commits
254 Commits
deprecate-
...
fix-msg-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cdee41c7e | ||
|
|
eda728eb68 | ||
|
|
2191fe465c | ||
|
|
78ae8f2a51 | ||
|
|
569a911455 | ||
|
|
39ff880506 | ||
|
|
c5b95f5a4b | ||
|
|
209157c9dd | ||
|
|
15f4aebcd5 | ||
|
|
2354c52b16 | ||
|
|
fb59d68edd | ||
|
|
227d0fa7dc | ||
|
|
7b854fb5ca | ||
|
|
7c1eff54fb | ||
|
|
f8b160595f | ||
|
|
c92fa6aa8a | ||
|
|
77acbc6814 | ||
|
|
81cb1e427f | ||
|
|
f6ba9604a7 | ||
|
|
9c6544ebfa | ||
|
|
b6eeccadeb | ||
|
|
37d14f942e | ||
|
|
4594ae474e | ||
|
|
f26e657577 | ||
|
|
e7b7479589 | ||
|
|
a6b29541df | ||
|
|
175357f576 | ||
|
|
e1634076f2 | ||
|
|
d6df664102 | ||
|
|
50a5b36498 | ||
|
|
a25bfd264c | ||
|
|
4d6fe936ae | ||
|
|
ec9f3fa6ea | ||
|
|
8356ad97e4 | ||
|
|
bf51c38975 | ||
|
|
3df3c876cc | ||
|
|
f825e61b89 | ||
|
|
b32293f2cb | ||
|
|
64cd62d6af | ||
|
|
68f07c5f9d | ||
|
|
7fb96ce2ba | ||
|
|
12687a1073 | ||
|
|
89de499198 | ||
|
|
4881362340 | ||
|
|
f31fd34ce0 | ||
|
|
18000ccf21 | ||
|
|
7776ec15b6 | ||
|
|
e4c7fca716 | ||
|
|
5b63bd9331 | ||
|
|
289f90bdbe | ||
|
|
26bcc9627d | ||
|
|
09a0df3a1f | ||
|
|
fe329892de | ||
|
|
2681332678 | ||
|
|
f994eb185f | ||
|
|
cc37535b2d | ||
|
|
55c23dec13 | ||
|
|
4dfc062abd | ||
|
|
ced334d13b | ||
|
|
0be21d90c1 | ||
|
|
521fbc44b4 | ||
|
|
361771c9bb | ||
|
|
fa45660b7d | ||
|
|
2e8f4ad6af | ||
|
|
18550ea80c | ||
|
|
1c1c0cc791 | ||
|
|
a0c0388dd9 | ||
|
|
789c1ab59d | ||
|
|
e8367894f2 | ||
|
|
8aae4f1b9d | ||
|
|
5850a7cd6b | ||
|
|
6c89ea7cee | ||
|
|
8a8f60d129 | ||
|
|
b59409bec0 | ||
|
|
c66125114f | ||
|
|
edb7ec58c6 | ||
|
|
655c6b51fe | ||
|
|
0bd4cefad3 | ||
|
|
0952007805 | ||
|
|
9b1fb795d7 | ||
|
|
3040e5a7bb | ||
|
|
7612799ef6 | ||
|
|
3b82d55176 | ||
|
|
a6b8202cd4 | ||
|
|
cfc1bf10c9 | ||
|
|
c5fad6cca1 | ||
|
|
b8d7222423 | ||
|
|
7d1300ab66 | ||
|
|
16d7de5989 | ||
|
|
102c447fe3 | ||
|
|
d66665b96e | ||
|
|
088be6bf6a | ||
|
|
bd3cbfc1ad | ||
|
|
fddc4e00ca | ||
|
|
5f7eec5504 | ||
|
|
6b94c297b9 | ||
|
|
edeb25cab5 | ||
|
|
44688e8363 | ||
|
|
ca79760372 | ||
|
|
4a669032dc | ||
|
|
b53dd2ec90 | ||
|
|
5ae4ff9162 | ||
|
|
a0e14439cb | ||
|
|
ed394f5f9d | ||
|
|
11db6d4dcc | ||
|
|
10c6836263 | ||
|
|
4e03df5ea7 | ||
|
|
9b41131af8 | ||
|
|
fb34dac08d | ||
|
|
d3e3a91096 | ||
|
|
5f8503c62d | ||
|
|
b0e8321514 | ||
|
|
dd2f77ea0c | ||
|
|
6c7cff7de2 | ||
|
|
46f797c40d | ||
|
|
75b01e17bc | ||
|
|
8685436cbb | ||
|
|
834c3c5cc2 | ||
|
|
25a19b49ad | ||
|
|
a4d96bebfb | ||
|
|
d21d6d2085 | ||
|
|
26c38ffc8e | ||
|
|
237b8908f7 | ||
|
|
06bccef462 | ||
|
|
3120bb8fd7 | ||
|
|
0903ed8232 | ||
|
|
f8ba392a24 | ||
|
|
3dd384dd53 | ||
|
|
2c071a3283 | ||
|
|
596cd7e0b6 | ||
|
|
3f5c30e3b3 | ||
|
|
1a279c6053 | ||
|
|
67e3a17b28 | ||
|
|
24204feb71 | ||
|
|
4ace2638e1 | ||
|
|
5aa486d6c2 | ||
|
|
ba26d03b1b | ||
|
|
9a1c2c9b61 | ||
|
|
5b9db81819 | ||
|
|
f2ba7d7851 | ||
|
|
1eafdfcbc8 | ||
|
|
3d825c51dd | ||
|
|
915f882e1f | ||
|
|
103ea2f168 | ||
|
|
4fef890466 | ||
|
|
35f5b7ec03 | ||
|
|
5136c8ba24 | ||
|
|
1037fa5622 | ||
|
|
8b42bf7a95 | ||
|
|
1c329d9ffa | ||
|
|
093a37a2b0 | ||
|
|
1daf5aad1f | ||
|
|
fe3f14a63e | ||
|
|
7574bfb7cb | ||
|
|
ce75bf4496 | ||
|
|
5ce47045e7 | ||
|
|
57e1725419 | ||
|
|
11309662a9 | ||
|
|
890357d579 | ||
|
|
f413c49555 | ||
|
|
c19f573b49 | ||
|
|
5de61b1a3d | ||
|
|
1c1462e776 | ||
|
|
eb6ef1cbea | ||
|
|
9654f5b218 | ||
|
|
68726a1b0e | ||
|
|
5b62bbe8e6 | ||
|
|
e55084629a | ||
|
|
1691e885f2 | ||
|
|
2d7818797d | ||
|
|
f65e2c639e | ||
|
|
95200e8f6b | ||
|
|
36e8dc74f4 | ||
|
|
78c5309e9a | ||
|
|
9feb1d378e | ||
|
|
e5e8683cdb | ||
|
|
d538ad170c | ||
|
|
c64c196778 | ||
|
|
8e552a9f0c | ||
|
|
a02017a5c8 | ||
|
|
0046d957f1 | ||
|
|
4a241deb96 | ||
|
|
8d5ae1d5d2 | ||
|
|
e1e89a5e62 | ||
|
|
a7be93449e | ||
|
|
c8694f9f2d | ||
|
|
062168cd42 | ||
|
|
1877a2c531 | ||
|
|
52f0e5a3db | ||
|
|
ac8c372349 | ||
|
|
1bfa429c38 | ||
|
|
ddd149945a | ||
|
|
e3dd8164a4 | ||
|
|
9b8149f14e | ||
|
|
05f1518951 | ||
|
|
e26de85b5f | ||
|
|
a2df80e833 | ||
|
|
db238ef524 | ||
|
|
f2b935f48f | ||
|
|
e69da71d4e | ||
|
|
7505fe7a7c | ||
|
|
f6857f1bcb | ||
|
|
7fe2c74139 | ||
|
|
be60f9612e | ||
|
|
2de9f015b1 | ||
|
|
c1f4f79d4a | ||
|
|
7b874cf597 | ||
|
|
8568b56ac6 | ||
|
|
f2a880f813 | ||
|
|
691327b2db | ||
|
|
a23c58c10a | ||
|
|
27c6b24e3a | ||
|
|
384436e937 | ||
|
|
eb30aae486 | ||
|
|
079286da04 | ||
|
|
0130899b3b | ||
|
|
d1f3c3c982 | ||
|
|
3b6eefa8bb | ||
|
|
5107531425 | ||
|
|
88655ffc44 | ||
|
|
10bd10b9d1 | ||
|
|
956a0f102b | ||
|
|
bdedd0e1fe | ||
|
|
4c901033b2 | ||
|
|
7d926da98c | ||
|
|
1b793d1f23 | ||
|
|
b5a8e8f51b | ||
|
|
cc5d00e211 | ||
|
|
1a8ab2aadc | ||
|
|
608fdc6f52 | ||
|
|
1d8638b47d | ||
|
|
3ecff48722 | ||
|
|
aa3b14ce72 | ||
|
|
28aeb0f09e | ||
|
|
7c5e2c5393 | ||
|
|
df8b629c2c | ||
|
|
a506dc6b65 | ||
|
|
fc1e6ccb8c | ||
|
|
bbc638ab82 | ||
|
|
4f57a2e248 | ||
|
|
4c6db2c5bd | ||
|
|
bbe548bc98 | ||
|
|
d1fbf65c5d | ||
|
|
7a4a915312 | ||
|
|
4f895f744b | ||
|
|
66a831dfa8 | ||
|
|
516597a73e | ||
|
|
4eb6c9fb8e | ||
|
|
46e2ae8860 | ||
|
|
54c0cbeb66 | ||
|
|
82ddf4732a | ||
|
|
ed0cdefb44 | ||
|
|
8836be0f47 | ||
|
|
96f63f3945 |
@@ -76,7 +76,7 @@ bool loopCanSleep()
|
||||
// Called just prior to starting Meshtastic. Allows for setting config values before startup.
|
||||
void lateInitVariant()
|
||||
{
|
||||
settingsMap[logoutputlevel] = level_error;
|
||||
portduino_config.logoutputlevel = level_error;
|
||||
channelFile.channels[0] = meshtastic_Channel{
|
||||
.has_settings = true,
|
||||
.settings =
|
||||
@@ -132,7 +132,7 @@ int portduino_main(int argc, char **argv); // Renamed "main" function from Mesht
|
||||
// Start Meshtastic in a thread and wait till it has reached the ON state.
|
||||
int LLVMFuzzerInitialize(int *argc, char ***argv)
|
||||
{
|
||||
settingsMap[maxtophone] = 5;
|
||||
portduino_config.maxtophone = 5;
|
||||
|
||||
meshtasticThread = std::thread([program = *argv[0]]() {
|
||||
char nodeIdStr[12];
|
||||
|
||||
4
.github/actions/setup-base/action.yml
vendored
4
.github/actions/setup-base/action.yml
vendored
@@ -5,7 +5,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
@@ -23,7 +23,7 @@ runs:
|
||||
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
cache: pip
|
||||
|
||||
2
.github/workflows/build_debian_src.yml
vendored
2
.github/workflows/build_debian_src.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
path: meshtasticd
|
||||
|
||||
42
.github/workflows/build_esp32.yml
vendored
42
.github/workflows/build_esp32.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: Build ESP32
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
board:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
build-esp32:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Build ESP32
|
||||
id: build
|
||||
uses: meshtastic/gh-action-firmware@main
|
||||
with:
|
||||
pio_platform: esp32
|
||||
pio_env: ${{ inputs.board }}
|
||||
pio_target: build
|
||||
ota_firmware_source: firmware.bin
|
||||
ota_firmware_target: release/bleota.bin
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: firmware-esp32-${{ inputs.board }}-${{ inputs.version }}.zip
|
||||
overwrite: true
|
||||
path: |
|
||||
release/*.bin
|
||||
release/*.elf
|
||||
42
.github/workflows/build_esp32_c3.yml
vendored
42
.github/workflows/build_esp32_c3.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: Build ESP32-C3
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
board:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
build-esp32-c3:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Build ESP32-C3
|
||||
id: build
|
||||
uses: meshtastic/gh-action-firmware@main
|
||||
with:
|
||||
pio_platform: esp32
|
||||
pio_env: ${{ inputs.board }}
|
||||
pio_target: build
|
||||
ota_firmware_source: firmware-c3.bin
|
||||
ota_firmware_target: release/bleota-c3.bin
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: firmware-esp32c3-${{ inputs.board }}-${{ inputs.version }}.zip
|
||||
overwrite: true
|
||||
path: |
|
||||
release/*.bin
|
||||
release/*.elf
|
||||
42
.github/workflows/build_esp32_c6.yml
vendored
42
.github/workflows/build_esp32_c6.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: Build ESP32-C6
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
board:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
build-esp32-c6:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Build ESP32-C6
|
||||
id: build
|
||||
uses: meshtastic/gh-action-firmware@main
|
||||
with:
|
||||
pio_platform: esp32
|
||||
pio_env: ${{ inputs.board }}
|
||||
pio_target: build
|
||||
ota_firmware_source: firmware-c3.bin
|
||||
ota_firmware_target: release/bleota-c3.bin
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: firmware-esp32c6-${{ inputs.board }}-${{ inputs.version }}.zip
|
||||
overwrite: true
|
||||
path: |
|
||||
release/*.bin
|
||||
release/*.elf
|
||||
42
.github/workflows/build_esp32_s3.yml
vendored
42
.github/workflows/build_esp32_s3.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: Build ESP32-S3
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
board:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
build-esp32-s3:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Build ESP32-S3
|
||||
id: build
|
||||
uses: meshtastic/gh-action-firmware@main
|
||||
with:
|
||||
pio_platform: esp32
|
||||
pio_env: ${{ inputs.board }}
|
||||
pio_target: build
|
||||
ota_firmware_source: firmware-s3.bin
|
||||
ota_firmware_target: release/bleota-s3.bin
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: firmware-esp32s3-${{ inputs.board }}-${{ inputs.version }}.zip
|
||||
overwrite: true
|
||||
path: |
|
||||
release/*.bin
|
||||
release/*.elf
|
||||
66
.github/workflows/build_firmware.yml
vendored
Normal file
66
.github/workflows/build_firmware.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
platform:
|
||||
required: true
|
||||
type: string
|
||||
pio_env:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
pio-build:
|
||||
name: build-${{ inputs.platform }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Set OTA firmware source and target
|
||||
if: startsWith(inputs.platform, 'esp32')
|
||||
id: ota_dir
|
||||
env:
|
||||
PIO_PLATFORM: ${{ inputs.platform }}
|
||||
run: |
|
||||
if [ "$PIO_PLATFORM" = "esp32s3" ]; then
|
||||
echo "src=firmware-s3.bin" >> $GITHUB_OUTPUT
|
||||
echo "tgt=release/bleota-s3.bin" >> $GITHUB_OUTPUT
|
||||
elif [ "$PIO_PLATFORM" = "esp32c3" ] || [ "$PIO_PLATFORM" = "esp32c6" ]; then
|
||||
echo "src=firmware-c3.bin" >> $GITHUB_OUTPUT
|
||||
echo "tgt=release/bleota-c3.bin" >> $GITHUB_OUTPUT
|
||||
elif [ "$PIO_PLATFORM" = "esp32" ]; then
|
||||
echo "src=firmware.bin" >> $GITHUB_OUTPUT
|
||||
echo "tgt=release/bleota.bin" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build ${{ inputs.platform }}
|
||||
id: build
|
||||
uses: meshtastic/gh-action-firmware@main
|
||||
with:
|
||||
pio_platform: ${{ inputs.platform }}
|
||||
pio_env: ${{ inputs.pio_env }}
|
||||
pio_target: build
|
||||
ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }}
|
||||
ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }}
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}.zip
|
||||
overwrite: true
|
||||
path: |
|
||||
release/*.bin
|
||||
release/*.elf
|
||||
release/*.uf2
|
||||
release/*.hex
|
||||
release/*-ota.zip
|
||||
42
.github/workflows/build_nrf52.yml
vendored
42
.github/workflows/build_nrf52.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: Build NRF52
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
board:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
build-nrf52:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Build NRF52
|
||||
id: build
|
||||
uses: meshtastic/gh-action-firmware@main
|
||||
with:
|
||||
pio_platform: nrf52
|
||||
pio_env: ${{ inputs.board }}
|
||||
pio_target: build
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: firmware-nrf52840-${{ inputs.board }}-${{ inputs.version }}.zip
|
||||
overwrite: true
|
||||
path: |
|
||||
release/*.uf2
|
||||
release/*.elf
|
||||
release/*.hex
|
||||
release/*-ota.zip
|
||||
40
.github/workflows/build_rpi2040.yml
vendored
40
.github/workflows/build_rpi2040.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Build RPI2040
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
board:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
build-rpi2040:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Build Raspberry Pi 2040
|
||||
id: build
|
||||
uses: meshtastic/gh-action-firmware@main
|
||||
with:
|
||||
pio_platform: rp2xx0
|
||||
pio_env: ${{ inputs.board }}
|
||||
pio_target: build
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: firmware-rp2040-${{ inputs.board }}-${{ inputs.version }}.zip
|
||||
overwrite: true
|
||||
path: |
|
||||
release/*.uf2
|
||||
release/*.elf
|
||||
41
.github/workflows/build_stm32.yml
vendored
41
.github/workflows/build_stm32.yml
vendored
@@ -1,41 +0,0 @@
|
||||
name: Build STM32
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
board:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
build-stm32:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Build STM32WL
|
||||
id: build
|
||||
uses: meshtastic/gh-action-firmware@main
|
||||
with:
|
||||
pio_platform: stm32wl
|
||||
pio_env: ${{ inputs.board }}
|
||||
pio_target: build
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: firmware-stm32-${{ inputs.board }}-${{ inputs.version }}.zip
|
||||
overwrite: true
|
||||
path: |
|
||||
release/*.hex
|
||||
release/*.bin
|
||||
release/*.elf
|
||||
2
.github/workflows/docker_build.yml
vendored
2
.github/workflows/docker_build.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
runs-on: ${{ inputs.runs-on }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
|
||||
2
.github/workflows/docker_manifest.yml
vendored
2
.github/workflows/docker_manifest.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
|
||||
2
.github/workflows/hook_copr.yml
vendored
2
.github/workflows/hook_copr.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
152
.github/workflows/main_matrix.yml
vendored
152
.github/workflows/main_matrix.yml
vendored
@@ -30,16 +30,35 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32, check]
|
||||
runs-on: ubuntu-latest
|
||||
arch:
|
||||
- esp32
|
||||
- esp32s3
|
||||
- esp32c3
|
||||
- esp32c6
|
||||
- nrf52840
|
||||
- rp2040
|
||||
- rp2350
|
||||
- stm32
|
||||
- check
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: jsonStep
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
cache: pip
|
||||
- run: pip install -U platformio
|
||||
- name: Uncomment build epoch
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
|
||||
- name: Generate matrix
|
||||
id: jsonStep
|
||||
run: |
|
||||
if [[ "$GITHUB_HEAD_REF" == "" ]]; then
|
||||
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}})
|
||||
else
|
||||
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} quick)
|
||||
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} pr)
|
||||
fi
|
||||
echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF Targets: $TARGETS"
|
||||
echo "${{matrix.arch}}=$(jq -cn --argjson environments "$TARGETS" '{board: $environments}')" >> $GITHUB_OUTPUT
|
||||
@@ -50,13 +69,14 @@ jobs:
|
||||
esp32c6: ${{ steps.jsonStep.outputs.esp32c6 }}
|
||||
nrf52840: ${{ steps.jsonStep.outputs.nrf52840 }}
|
||||
rp2040: ${{ steps.jsonStep.outputs.rp2040 }}
|
||||
rp2350: ${{ steps.jsonStep.outputs.rp2350 }}
|
||||
stm32: ${{ steps.jsonStep.outputs.stm32 }}
|
||||
check: ${{ steps.jsonStep.outputs.check }}
|
||||
|
||||
version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Get release version string
|
||||
run: |
|
||||
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
||||
@@ -77,7 +97,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Build base
|
||||
id: base
|
||||
uses: ./.github/actions/setup-base
|
||||
@@ -89,70 +109,88 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.setup.outputs.esp32) }}
|
||||
uses: ./.github/workflows/build_esp32.yml
|
||||
uses: ./.github/workflows/build_firmware.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.long }}
|
||||
board: ${{ matrix.board }}
|
||||
pio_env: ${{ matrix.board }}
|
||||
platform: esp32
|
||||
|
||||
build-esp32-s3:
|
||||
build-esp32s3:
|
||||
needs: [setup, version]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }}
|
||||
uses: ./.github/workflows/build_esp32_s3.yml
|
||||
uses: ./.github/workflows/build_firmware.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.long }}
|
||||
board: ${{ matrix.board }}
|
||||
pio_env: ${{ matrix.board }}
|
||||
platform: esp32s3
|
||||
|
||||
build-esp32-c3:
|
||||
build-esp32c3:
|
||||
needs: [setup, version]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }}
|
||||
uses: ./.github/workflows/build_esp32_c3.yml
|
||||
uses: ./.github/workflows/build_firmware.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.long }}
|
||||
board: ${{ matrix.board }}
|
||||
pio_env: ${{ matrix.board }}
|
||||
platform: esp32c3
|
||||
|
||||
build-esp32-c6:
|
||||
build-esp32c6:
|
||||
needs: [setup, version]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }}
|
||||
uses: ./.github/workflows/build_esp32_c6.yml
|
||||
uses: ./.github/workflows/build_firmware.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.long }}
|
||||
board: ${{ matrix.board }}
|
||||
pio_env: ${{ matrix.board }}
|
||||
platform: esp32c6
|
||||
|
||||
build-nrf52:
|
||||
build-nrf52840:
|
||||
needs: [setup, version]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }}
|
||||
uses: ./.github/workflows/build_nrf52.yml
|
||||
uses: ./.github/workflows/build_firmware.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.long }}
|
||||
board: ${{ matrix.board }}
|
||||
pio_env: ${{ matrix.board }}
|
||||
platform: nrf52840
|
||||
|
||||
build-rpi2040:
|
||||
build-rp2040:
|
||||
needs: [setup, version]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.setup.outputs.rp2040) }}
|
||||
uses: ./.github/workflows/build_rpi2040.yml
|
||||
uses: ./.github/workflows/build_firmware.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.long }}
|
||||
board: ${{ matrix.board }}
|
||||
pio_env: ${{ matrix.board }}
|
||||
platform: rp2040
|
||||
|
||||
build-rp2350:
|
||||
needs: [setup, version]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.setup.outputs.rp2350) }}
|
||||
uses: ./.github/workflows/build_firmware.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.long }}
|
||||
pio_env: ${{ matrix.board }}
|
||||
platform: rp2350
|
||||
|
||||
build-stm32:
|
||||
needs: [setup, version]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.setup.outputs.stm32) }}
|
||||
uses: ./.github/workflows/build_stm32.yml
|
||||
uses: ./.github/workflows/build_firmware.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.long }}
|
||||
board: ${{ matrix.board }}
|
||||
pio_env: ${{ matrix.board }}
|
||||
platform: stm32
|
||||
|
||||
build-debian-src:
|
||||
if: github.repository == 'meshtastic/firmware'
|
||||
@@ -230,27 +268,36 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32]
|
||||
arch:
|
||||
- esp32
|
||||
- esp32s3
|
||||
- esp32c3
|
||||
- esp32c6
|
||||
- nrf52840
|
||||
- rp2040
|
||||
- rp2350
|
||||
- stm32
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
[
|
||||
version,
|
||||
build-esp32,
|
||||
build-esp32-s3,
|
||||
build-esp32-c3,
|
||||
build-esp32-c6,
|
||||
build-nrf52,
|
||||
build-rpi2040,
|
||||
build-esp32s3,
|
||||
build-esp32c3,
|
||||
build-esp32c6,
|
||||
build-nrf52840,
|
||||
build-rp2040,
|
||||
build-rp2350,
|
||||
build-stm32,
|
||||
]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: ./
|
||||
pattern: firmware-${{matrix.arch}}-*
|
||||
@@ -279,7 +326,7 @@ jobs:
|
||||
./Meshtastic_nRF52_factory_erase*.uf2
|
||||
retention-days: 30
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -324,10 +371,10 @@ jobs:
|
||||
- package-pio-deps-native-tft
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
@@ -343,14 +390,14 @@ jobs:
|
||||
Autogenerated by github action, developer should edit as required before publishing...
|
||||
|
||||
- name: Download source deb
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
|
||||
merge-multiple: true
|
||||
path: ./output/debian-src
|
||||
|
||||
- name: Download `native-tft` pio deps
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -379,20 +426,28 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32]
|
||||
arch:
|
||||
- esp32
|
||||
- esp32s3
|
||||
- esp32c3
|
||||
- esp32c6
|
||||
- nrf52840
|
||||
- rp2040
|
||||
- rp2350
|
||||
- stm32
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
needs: [release-artifacts, version]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -409,7 +464,7 @@ jobs:
|
||||
- name: Zip firmware
|
||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||
merge-multiple: true
|
||||
@@ -436,17 +491,18 @@ jobs:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
needs: [release-firmware, version]
|
||||
env:
|
||||
targets: esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,stm32
|
||||
targets: |-
|
||||
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
|
||||
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Trunk Check
|
||||
uses: trunk-io/trunk-action@v1
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
pull-requests: write # For trunk to create PRs
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Trunk Upgrade
|
||||
uses: trunk-io/trunk-action/upgrade@v1
|
||||
|
||||
4
.github/workflows/package_obs.yml
vendored
4
.github/workflows/package_obs.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
needs: build-debian-src
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
path: meshtasticd
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
id: version
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
||||
merge-multiple: true
|
||||
|
||||
4
.github/workflows/package_pio_deps.yml
vendored
4
.github/workflows/package_pio_deps.yml
vendored
@@ -24,14 +24,14 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
|
||||
4
.github/workflows/package_ppa.yml
vendored
4
.github/workflows/package_ppa.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
needs: build-debian-src
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
path: meshtasticd
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
id: version
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
||||
merge-multiple: true
|
||||
|
||||
2
.github/workflows/pr_enforce_labels.yml
vendored
2
.github/workflows/pr_enforce_labels.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check for PR labels
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const labels = context.payload.pull_request.labels.map(label => label.name);
|
||||
|
||||
238
.github/workflows/pr_tests.yml
vendored
Normal file
238
.github/workflows/pr_tests.yml
vendored
Normal file
@@ -0,0 +1,238 @@
|
||||
name: Tests
|
||||
|
||||
# DISABLED: Changed from automatic PR triggers to manual only
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: "Reason for manual test run"
|
||||
required: false
|
||||
default: "Manual test execution"
|
||||
|
||||
concurrency:
|
||||
group: tests-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
native-tests:
|
||||
name: "🧪 Native Tests"
|
||||
if: github.repository == 'meshtastic/firmware'
|
||||
uses: ./.github/workflows/test_native.yml
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
|
||||
test-summary:
|
||||
name: "📊 Test Results"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [native-tests]
|
||||
if: always()
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Get release version string
|
||||
run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
||||
id: version
|
||||
|
||||
- name: Download test artifacts
|
||||
if: needs.native-tests.result != 'skipped'
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: platformio-test-report-${{ steps.version.outputs.long }}.zip
|
||||
merge-multiple: true
|
||||
|
||||
- name: Parse test results and create detailed summary
|
||||
id: test-results
|
||||
run: |
|
||||
echo "## 🧪 Test Results Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Check overall job status first
|
||||
if [[ "${{ needs.native-tests.result }}" == "success" ]]; then
|
||||
echo "✅ **Overall Status**: PASSED" >> $GITHUB_STEP_SUMMARY
|
||||
elif [[ "${{ needs.native-tests.result }}" == "failure" ]]; then
|
||||
echo "❌ **Overall Status**: FAILED" >> $GITHUB_STEP_SUMMARY
|
||||
elif [[ "${{ needs.native-tests.result }}" == "cancelled" ]]; then
|
||||
echo "⏸️ **Overall Status**: CANCELLED" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Tests were cancelled before completion." >> $GITHUB_STEP_SUMMARY
|
||||
exit 0
|
||||
else
|
||||
echo "⚠️ **Overall Status**: SKIPPED" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Tests were skipped." >> $GITHUB_STEP_SUMMARY
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Parse detailed test results if available
|
||||
if [ -f "testreport.xml" ]; then
|
||||
echo "### 🔍 Individual Test Results" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
python3 << 'EOF'
|
||||
import xml.etree.ElementTree as ET
|
||||
import os
|
||||
|
||||
try:
|
||||
tree = ET.parse('testreport.xml')
|
||||
root = tree.getroot()
|
||||
|
||||
total_tests = 0
|
||||
passed_tests = 0
|
||||
failed_tests = 0
|
||||
skipped_tests = 0
|
||||
|
||||
# Parse testsuite elements
|
||||
for testsuite in root.findall('.//testsuite'):
|
||||
suite_name = testsuite.get('name', 'Unknown')
|
||||
suite_tests = int(testsuite.get('tests', '0'))
|
||||
suite_failures = int(testsuite.get('failures', '0'))
|
||||
suite_errors = int(testsuite.get('errors', '0'))
|
||||
suite_skipped = int(testsuite.get('skipped', '0'))
|
||||
|
||||
total_tests += suite_tests
|
||||
failed_tests += suite_failures + suite_errors
|
||||
skipped_tests += suite_skipped
|
||||
passed_tests += suite_tests - suite_failures - suite_errors - suite_skipped
|
||||
|
||||
if suite_tests > 0:
|
||||
status = "✅" if (suite_failures + suite_errors) == 0 else "❌"
|
||||
print(f"**{status} Test Suite: {suite_name}**")
|
||||
print(f"- Total: {suite_tests}")
|
||||
print(f"- Passed: ✅ {suite_tests - suite_failures - suite_errors - suite_skipped}")
|
||||
print(f"- Failed: ❌ {suite_failures + suite_errors}")
|
||||
if suite_skipped > 0:
|
||||
print(f"- Skipped: ⏭️ {suite_skipped}")
|
||||
print("")
|
||||
|
||||
# Show individual test results for failed suites
|
||||
if suite_failures + suite_errors > 0:
|
||||
print("**Failed Tests:**")
|
||||
for testcase in testsuite.findall('testcase'):
|
||||
test_name = testcase.get('name', 'Unknown')
|
||||
failure = testcase.find('failure')
|
||||
error = testcase.find('error')
|
||||
|
||||
if failure is not None:
|
||||
msg = failure.get('message', 'Unknown error')[:100]
|
||||
print(f"- ❌ `{test_name}`: {msg}")
|
||||
elif error is not None:
|
||||
msg = error.get('message', 'Unknown error')[:100]
|
||||
print(f"- ❌ `{test_name}`: ERROR - {msg}")
|
||||
print("")
|
||||
else:
|
||||
# Show passed tests for successful suites
|
||||
passed_count = 0
|
||||
for testcase in testsuite.findall('testcase'):
|
||||
if testcase.find('failure') is None and testcase.find('error') is None:
|
||||
if passed_count < 5: # Limit to first 5 to avoid spam
|
||||
test_name = testcase.get('name', 'Unknown')
|
||||
print(f"- ✅ `{test_name}`: PASSED")
|
||||
passed_count += 1
|
||||
if passed_count > 5:
|
||||
print(f"- ... and {passed_count - 5} more tests passed")
|
||||
print("")
|
||||
|
||||
# Summary statistics
|
||||
print("### 📊 Test Statistics")
|
||||
print(f"- **Total Tests**: {total_tests}")
|
||||
print(f"- **Passed**: ✅ {passed_tests}")
|
||||
print(f"- **Failed**: ❌ {failed_tests}")
|
||||
if skipped_tests > 0:
|
||||
print(f"- **Skipped**: ⏭️ {skipped_tests}")
|
||||
|
||||
if failed_tests > 0:
|
||||
print(f"\n❌ **{failed_tests} tests failed out of {total_tests} total**")
|
||||
else:
|
||||
print(f"\n✅ **All {total_tests} tests passed!**")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error parsing test results: {e}")
|
||||
EOF
|
||||
else
|
||||
echo "⚠️ **No detailed test report available**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Test artifacts may not have been generated properly." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "---" >> $GITHUB_STEP_SUMMARY
|
||||
echo "View detailed logs in the [Actions tab](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Comment test results on PR
|
||||
if: github.event_name == 'pull_request' && needs.native-tests.result != 'skipped'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
// Read the step summary to use as PR comment
|
||||
let testSummary = "## 🧪 Test Results Summary\n\n";
|
||||
|
||||
if ("${{ needs.native-tests.result }}" === "success") {
|
||||
testSummary += "✅ **All tests passed!**\n\n";
|
||||
} else if ("${{ needs.native-tests.result }}" === "failure") {
|
||||
testSummary += "❌ **Some tests failed.**\n\n";
|
||||
} else {
|
||||
testSummary += "⚠️ **Tests did not complete normally.**\n\n";
|
||||
}
|
||||
|
||||
testSummary += `View detailed results: [Actions Run](${context.payload.repository.html_url}/actions/runs/${context.runId})\n\n`;
|
||||
testSummary += "---\n";
|
||||
testSummary += "*This comment will be automatically updated when new commits are pushed.*";
|
||||
|
||||
// Find existing comment
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
|
||||
const botComment = comments.data.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes('🧪 Test Results Summary')
|
||||
);
|
||||
|
||||
if (botComment) {
|
||||
// Update existing comment
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: testSummary
|
||||
});
|
||||
} else {
|
||||
// Create new comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: testSummary
|
||||
});
|
||||
}
|
||||
|
||||
- name: Set overall status
|
||||
run: |
|
||||
if [[ "${{ needs.native-tests.result }}" == "success" ]]; then
|
||||
echo "All tests passed! ✅"
|
||||
exit 0
|
||||
else
|
||||
echo "Some tests failed! ❌"
|
||||
exit 1
|
||||
fi
|
||||
4
.github/workflows/release_channels.yml
vendored
4
.github/workflows/release_channels.yml
vendored
@@ -60,10 +60,10 @@ jobs:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
|
||||
2
.github/workflows/sec_sast_semgrep_cron.yml
vendored
2
.github/workflows/sec_sast_semgrep_cron.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
steps:
|
||||
# step 1
|
||||
- name: clone application source code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# step 2
|
||||
- name: full scan
|
||||
|
||||
2
.github/workflows/sec_sast_semgrep_pull.yml
vendored
2
.github/workflows/sec_sast_semgrep_pull.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
steps:
|
||||
# step 1
|
||||
- name: clone application source code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
2
.github/workflows/stale_bot.yml
vendored
2
.github/workflows/stale_bot.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Stale PR+Issues
|
||||
uses: actions/stale@v9.1.0
|
||||
uses: actions/stale@v10.0.0
|
||||
with:
|
||||
days-before-stale: 45
|
||||
exempt-issue-labels: pinned,3.0
|
||||
|
||||
10
.github/workflows/test_native.yml
vendored
10
.github/workflows/test_native.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
name: Native Simulator Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
name: Native PlatformIO Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
- platformio-tests
|
||||
if: always()
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
id: version
|
||||
|
||||
- name: Download test artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: platformio-test-report-${{ steps.version.outputs.long }}.zip
|
||||
merge-multiple: true
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
reporter: java-junit
|
||||
|
||||
- name: Download coverage artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}.zip
|
||||
path: code-coverage-report
|
||||
|
||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: test-runner
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# - uses: actions/setup-python@v5
|
||||
# with:
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
pio upgrade
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
|
||||
2
.github/workflows/trunk_annotate_pr.yml
vendored
2
.github/workflows/trunk_annotate_pr.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Trunk Check
|
||||
uses: trunk-io/trunk-action@v1
|
||||
|
||||
2
.github/workflows/trunk_check.yml
vendored
2
.github/workflows/trunk_check.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Trunk Check
|
||||
uses: trunk-io/trunk-action@v1
|
||||
|
||||
4
.github/workflows/trunk_format_pr.yml
vendored
4
.github/workflows/trunk_format_pr.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
git push
|
||||
|
||||
- name: Comment on PR
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/update_protobufs.yml
vendored
2
.github/workflows/update_protobufs.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
version: 0.1
|
||||
cli:
|
||||
version: 1.24.0
|
||||
version: 1.25.0
|
||||
plugins:
|
||||
sources:
|
||||
- id: trunk
|
||||
ref: v1.7.1
|
||||
ref: v1.7.2
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.451
|
||||
- renovate@41.40.0
|
||||
- checkov@3.2.469
|
||||
- renovate@41.94.0
|
||||
- prettier@3.6.2
|
||||
- trufflehog@3.90.1
|
||||
- trufflehog@3.90.5
|
||||
- yamllint@1.37.1
|
||||
- bandit@1.8.6
|
||||
- trivy@0.64.1
|
||||
- taplo@0.9.3
|
||||
- ruff@0.12.4
|
||||
- trivy@0.66.0
|
||||
- taplo@0.10.0
|
||||
- ruff@0.12.11
|
||||
- isort@6.0.1
|
||||
- markdownlint@0.45.0
|
||||
- oxipng@9.1.5
|
||||
- svgo@4.0.0
|
||||
- actionlint@1.7.7
|
||||
- flake8@7.3.0
|
||||
- hadolint@2.12.1-beta
|
||||
- hadolint@2.13.1
|
||||
- shfmt@3.6.0
|
||||
- shellcheck@0.10.0
|
||||
- shellcheck@0.11.0
|
||||
- black@25.1.0
|
||||
- git-diff-check
|
||||
- gitleaks@8.28.0
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -3,7 +3,7 @@
|
||||
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
|
||||
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||
|
||||
FROM python:3.13-bookworm AS builder
|
||||
FROM python:3.13-slim-trixie AS builder
|
||||
ARG PIO_ENV=native
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV TZ=Etc/UTC
|
||||
@@ -36,7 +36,7 @@ RUN curl -L "https://github.com/meshtastic/web/releases/download/v$(cat /tmp/fir
|
||||
|
||||
##### PRODUCTION BUILD #############
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
FROM debian:trixie-slim
|
||||
LABEL org.opencontainers.image.title="Meshtastic" \
|
||||
org.opencontainers.image.description="Debian Meshtastic daemon and web interface" \
|
||||
org.opencontainers.image.url="https://meshtastic.org" \
|
||||
@@ -51,8 +51,8 @@ ENV TZ=Etc/UTC
|
||||
USER root
|
||||
|
||||
RUN apt-get update && apt-get --no-install-recommends -y install \
|
||||
libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libuv1 libusb-1.0-0-dev \
|
||||
liborcania2.3 libulfius2.7 libssl3 \
|
||||
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
|
||||
liborcania2.3 libulfius2.7t64 libssl3t64 \
|
||||
libx11-6 libinput10 libxkbcommon-x11-0 \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p /var/lib/meshtasticd \
|
||||
@@ -61,7 +61,7 @@ RUN apt-get update && apt-get --no-install-recommends -y install \
|
||||
|
||||
# Fetch compiled binary from the builder
|
||||
COPY --from=builder /tmp/firmware/release/meshtasticd /usr/bin/
|
||||
COPY --from=builder /tmp/web /usr/share/meshtasticd/
|
||||
COPY --from=builder /tmp/web /usr/share/meshtasticd/web/
|
||||
# Copy config templates
|
||||
COPY ./bin/config.d /etc/meshtasticd/available.d
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ build_flags =
|
||||
-DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
|
||||
|
||||
build_src_filter =
|
||||
${arduino_base.build_src_filter} -<platform/esp32/> -<platform/stm32wl> -<nimble/> -<mesh/wifi/> -<mesh/api/> -<mesh/http/> -<modules/esp32> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp>
|
||||
${arduino_base.build_src_filter} -<platform/esp32/> -<platform/stm32wl> -<nimble/> -<mesh/wifi/> -<mesh/api/> -<mesh/http/> -<modules/esp32> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp> -<serialization/>
|
||||
|
||||
lib_deps=
|
||||
${arduino_base.lib_deps}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
[portduino_base]
|
||||
platform =
|
||||
# renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop
|
||||
https://github.com/meshtastic/platform-native/archive/6cb7a455b440dd0738e8ed74a18136ed5cf7ea63.zip
|
||||
https://github.com/meshtastic/platform-native/archive/c490bcd019e0658404088a61b96e653c9da22c45.zip
|
||||
framework = arduino
|
||||
|
||||
build_src_filter =
|
||||
@@ -17,7 +17,6 @@ build_src_filter =
|
||||
+<mesh/raspihttp/>
|
||||
-<mesh/eth/>
|
||||
-<modules/esp32>
|
||||
+<../variants/portduino>
|
||||
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
@@ -32,9 +31,12 @@ lib_deps =
|
||||
https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip
|
||||
# renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library
|
||||
adafruit/Adafruit seesaw Library@1.7.9
|
||||
# renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main
|
||||
https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip
|
||||
|
||||
build_flags =
|
||||
${arduino_base.build_flags}
|
||||
-D ARCH_PORTDUINO
|
||||
-fPIC
|
||||
-Isrc/platform/portduino
|
||||
-DRADIOLIB_EEPROM_UNSUPPORTED
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
extends = arduino_base
|
||||
platform =
|
||||
# renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32
|
||||
platformio/ststm32@19.2.0
|
||||
platformio/ststm32@19.3.0
|
||||
platform_packages =
|
||||
# TODO renovate
|
||||
platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip
|
||||
@@ -50,7 +50,7 @@ lib_deps =
|
||||
${radiolib_base.lib_deps}
|
||||
|
||||
# renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main
|
||||
https://github.com/caveman99/Crypto/archive/eae9c768054118a9399690f8af202853d1ae8516.zip
|
||||
https://github.com/caveman99/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip
|
||||
|
||||
lib_ignore =
|
||||
OneButton
|
||||
|
||||
@@ -11,7 +11,7 @@ elif (echo $2 | grep -q "nrf52"); then
|
||||
elif (echo $2 | grep -q "stm32"); then
|
||||
bin/build-stm32.sh $1
|
||||
elif (echo $2 | grep -q "rpi2040"); then
|
||||
bin/build-rpi2040.sh $1
|
||||
bin/build-rp2xx0.sh $1
|
||||
else
|
||||
echo "Unknown target $2"
|
||||
exit 1
|
||||
|
||||
@@ -9,13 +9,4 @@ Lora:
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.0
|
||||
# CS: 8
|
||||
|
||||
|
||||
### RAK13300in Slot 2 pins
|
||||
# IRQ: 18 #IO6
|
||||
# Reset: 24 # IO4
|
||||
# Busy: 19 # IO5
|
||||
# # Ant_sw: 23 # IO3
|
||||
# spidev: spidev0.1
|
||||
# # CS: 7
|
||||
# CS: 8
|
||||
8
bin/config.d/lora-RAK6421-13300-slot2.yaml
Normal file
8
bin/config.d/lora-RAK6421-13300-slot2.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
Lora:
|
||||
### RAK13300in Slot 2 pins
|
||||
IRQ: 18 #IO6
|
||||
Reset: 24 # IO4
|
||||
Busy: 19 # IO5
|
||||
# Ant_sw: 23 # IO3
|
||||
spidev: spidev0.1
|
||||
# CS: 7
|
||||
@@ -7,6 +7,7 @@ SET "DEBUG=0"
|
||||
SET "PYTHON="
|
||||
SET "TFT_BUILD=0"
|
||||
SET "BIGDB8=0"
|
||||
SET "MUIDB8=0"
|
||||
SET "BIGDB16=0"
|
||||
SET "ESPTOOL_BAUD=115200"
|
||||
SET "ESPTOOL_CMD="
|
||||
@@ -14,11 +15,12 @@ SET "LOGCOUNTER=0"
|
||||
SET "BPS_RESET=0"
|
||||
|
||||
@REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable.
|
||||
SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone"
|
||||
SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone t-eth-elite tlora-pager mesh-tab dreamcatcher ESP32-S3-Pico seeed-sensecap-indicator heltec_capsule_sensor_v3 vision-master icarus tracksenger elecrow-adv"
|
||||
SET "C3=esp32c3"
|
||||
@REM FIXME: Determine flash size from PlatformIO variant, this is unmaintainable.
|
||||
SET "BIGDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger"
|
||||
SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite t-watch-s3"
|
||||
SET "BIGDB_8MB=crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger"
|
||||
SET "MUIDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator"
|
||||
SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite tlora-pager t-watch-s3 elecrow-adv"
|
||||
|
||||
GOTO getopts
|
||||
:help
|
||||
@@ -100,7 +102,6 @@ IF NOT "!FILENAME:update=!"=="!FILENAME!" (
|
||||
)
|
||||
|
||||
:skip-filename
|
||||
SET "ESPTOOL_BAUD=1200"
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
|
||||
IF NOT "__%PYTHON%__"=="____" (
|
||||
@@ -120,11 +121,10 @@ IF NOT "__%PYTHON%__"=="____" (
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
|
||||
!ESPTOOL_CMD! >nul 2>&1
|
||||
IF %ERRORLEVEL% GEQ 2 (
|
||||
@REM esptool exits with code 1 if help is displayed.
|
||||
IF %ERRORLEVEL% EQU 9009 (
|
||||
@REM 9009 = command not found on Windows
|
||||
CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
|
||||
EXIT /B 1
|
||||
GOTO eof
|
||||
)
|
||||
IF %DEBUG% EQU 1 (
|
||||
CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
|
||||
@@ -142,7 +142,7 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
|
||||
|
||||
IF %BPS_RESET% EQU 1 (
|
||||
@REM Attempt to change mode via 1200bps Reset.
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status
|
||||
CALL :RUN_ESPTOOL 1200 --after no_reset read_flash_status
|
||||
GOTO eof
|
||||
)
|
||||
|
||||
@@ -164,6 +164,15 @@ FOR %%a IN (%BIGDB_8MB%) DO (
|
||||
)
|
||||
:end_loop_bigdb_8mb
|
||||
|
||||
FOR %%a IN (%MUIDB_8MB%) DO (
|
||||
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
|
||||
@REM We are working with any of %MUIDB_8MB%.
|
||||
SET "MUIDB8=1"
|
||||
GOTO end_loop_muidb_8mb
|
||||
)
|
||||
)
|
||||
:end_loop_muidb_8mb
|
||||
|
||||
FOR %%a IN (%BIGDB_16MB%) DO (
|
||||
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
|
||||
@REM We are working with any of %BIGDB_16MB%.
|
||||
@@ -174,6 +183,7 @@ FOR %%a IN (%BIGDB_16MB%) DO (
|
||||
:end_loop_bigdb_16mb
|
||||
|
||||
IF %BIGDB8% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 8mb partition selected."
|
||||
IF %MUIDB8% EQU 1 CALL :LOG_MESSAGE INFO "MUIDB 8mb partition selected."
|
||||
IF %BIGDB16% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 16mb partition selected."
|
||||
|
||||
@REM Extract BASENAME from %FILENAME% for later use.
|
||||
@@ -218,6 +228,12 @@ IF %BIGDB8% EQU 1 (
|
||||
SET "SPIFFS_OFFSET=0x670000"
|
||||
)
|
||||
|
||||
@REM Offsets for MUIDB 8mb.
|
||||
IF %MUIDB8% EQU 1 (
|
||||
SET "OTA_OFFSET=0x5D0000"
|
||||
SET "SPIFFS_OFFSET=0x670000"
|
||||
)
|
||||
|
||||
@REM Offsets for BigDB 16mb.
|
||||
IF %BIGDB16% EQU 1 (
|
||||
SET "OTA_OFFSET=0x650000"
|
||||
|
||||
@@ -5,38 +5,43 @@ BPS_RESET=false
|
||||
TFT_BUILD=false
|
||||
MCU=""
|
||||
|
||||
# Constants
|
||||
RESET_BAUD=1200
|
||||
FIRMWARE_OFFSET=0x00
|
||||
|
||||
# Variant groups
|
||||
BIGDB_8MB=(
|
||||
"picomputer-s3"
|
||||
"unphone"
|
||||
"seeed-sensecap-indicator"
|
||||
"crowpanel-esp32s3"
|
||||
"heltec_capsule_sensor_v3"
|
||||
"heltec-v3"
|
||||
"heltec-vision-master-e213"
|
||||
"heltec-vision-master-e290"
|
||||
"heltec-vision-master-t190"
|
||||
"heltec-wireless-paper"
|
||||
"heltec-wireless-tracker"
|
||||
"heltec-wsl-v3"
|
||||
"icarus"
|
||||
"seeed-xiao-s3"
|
||||
"tbeam-s3-core"
|
||||
"tracksenger"
|
||||
"crowpanel-esp32s3"
|
||||
"heltec_capsule_sensor_v3"
|
||||
"heltec-v3"
|
||||
"heltec-vision-master-e213"
|
||||
"heltec-vision-master-e290"
|
||||
"heltec-vision-master-t190"
|
||||
"heltec-wireless-paper"
|
||||
"heltec-wireless-tracker"
|
||||
"heltec-wsl-v3"
|
||||
"icarus"
|
||||
"seeed-xiao-s3"
|
||||
"tbeam-s3-core"
|
||||
"tracksenger"
|
||||
)
|
||||
MUIDB_8MB=(
|
||||
"picomputer-s3"
|
||||
"unphone"
|
||||
"seeed-sensecap-indicator"
|
||||
)
|
||||
BIGDB_16MB=(
|
||||
"t-deck"
|
||||
"mesh-tab"
|
||||
"t-energy-s3"
|
||||
"dreamcatcher"
|
||||
"ESP32-S3-Pico"
|
||||
"m5stack-cores3"
|
||||
"station-g2"
|
||||
"t-deck"
|
||||
"mesh-tab"
|
||||
"t-energy-s3"
|
||||
"dreamcatcher"
|
||||
"ESP32-S3-Pico"
|
||||
"m5stack-cores3"
|
||||
"station-g2"
|
||||
"t-eth-elite"
|
||||
"tlora-pager"
|
||||
"t-watch-s3"
|
||||
"elecrow-adv-35-tft"
|
||||
"elecrow-adv-24-28-tft"
|
||||
"elecrow-adv1-43-50-70-tft"
|
||||
"elecrow-adv"
|
||||
)
|
||||
S3_VARIANTS=(
|
||||
"s3"
|
||||
@@ -47,6 +52,7 @@ S3_VARIANTS=(
|
||||
"station-g2"
|
||||
"unphone"
|
||||
"t-eth-elite"
|
||||
"tlora-pager"
|
||||
"mesh-tab"
|
||||
"dreamcatcher"
|
||||
"ESP32-S3-Pico"
|
||||
@@ -106,8 +112,8 @@ while [ $# -gt 0 ]; do
|
||||
shift
|
||||
;;
|
||||
--1200bps-reset)
|
||||
BPS_RESET=true
|
||||
;;
|
||||
BPS_RESET=true
|
||||
;;
|
||||
--) # Stop parsing options
|
||||
shift
|
||||
break
|
||||
@@ -121,7 +127,7 @@ while [ $# -gt 0 ]; do
|
||||
done
|
||||
|
||||
if [[ $BPS_RESET == true ]]; then
|
||||
$ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status
|
||||
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -158,6 +164,13 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
|
||||
fi
|
||||
done
|
||||
|
||||
for variant in "${MUIDB_8MB[@]}"; do
|
||||
if [ -z "${FILENAME##*"$variant"*}" ]; then
|
||||
OFFSET=0x670000
|
||||
OTA_OFFSET=0x5D0000
|
||||
fi
|
||||
done
|
||||
|
||||
# littlefs* offset for BigDB 16mb and OTA OFFSET.
|
||||
for variant in "${BIGDB_16MB[@]}"; do
|
||||
if [ -z "${FILENAME##*"$variant"*}" ]; then
|
||||
@@ -201,8 +214,8 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
|
||||
fi
|
||||
|
||||
echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
|
||||
$ESPTOOL_CMD erase_flash
|
||||
$ESPTOOL_CMD write_flash 0x00 "${FILENAME}"
|
||||
$ESPTOOL_CMD erase-flash
|
||||
$ESPTOOL_CMD write-flash $FIRMWARE_OFFSET "${FILENAME}"
|
||||
echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}"
|
||||
$ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}"
|
||||
echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}"
|
||||
|
||||
@@ -6,6 +6,8 @@ SET "SCRIPT_NAME=%~nx0"
|
||||
SET "DEBUG=0"
|
||||
SET "PYTHON="
|
||||
SET "ESPTOOL_BAUD=115200"
|
||||
SET "RESET_BAUD=1200"
|
||||
SET "UPDATE_OFFSET=0x10000"
|
||||
SET "ESPTOOL_CMD="
|
||||
SET "LOGCOUNTER=0"
|
||||
SET "CHANGE_MODE=0"
|
||||
@@ -85,14 +87,13 @@ IF "!FILENAME:update=!"=="!FILENAME!" (
|
||||
)
|
||||
|
||||
:skip-filename
|
||||
SET "ESPTOOL_BAUD=1200"
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
|
||||
IF NOT "__%PYTHON%__"=="____" (
|
||||
SET "ESPTOOL_CMD=!PYTHON! -m esptool"
|
||||
SET "ESPTOOL_CMD=""!PYTHON!"" -m esptool"
|
||||
CALL :LOG_MESSAGE DEBUG "Python interpreter supplied."
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool...
|
||||
CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..."
|
||||
WHERE esptool >nul 2>&1
|
||||
IF %ERRORLEVEL% EQU 0 (
|
||||
@REM WHERE exits with code 0 if esptool is found.
|
||||
@@ -105,11 +106,11 @@ IF NOT "__%PYTHON%__"=="____" (
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
|
||||
!ESPTOOL_CMD! >nul 2>&1
|
||||
IF %ERRORLEVEL% GEQ 2 (
|
||||
@REM esptool exits with code 1 if help is displayed.
|
||||
CALL :LOG_MESSAGE DEBUG "esptool exit code: %ERRORLEVEL%"
|
||||
IF %ERRORLEVEL% EQU 9009 (
|
||||
@REM 9009 = command not found on Windows
|
||||
CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
|
||||
EXIT /B 1
|
||||
GOTO eof
|
||||
)
|
||||
IF %DEBUG% EQU 1 (
|
||||
CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
|
||||
@@ -127,13 +128,13 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
|
||||
|
||||
IF %CHANGE_MODE% EQU 1 (
|
||||
@REM Attempt to change mode via 1200bps Reset.
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status
|
||||
CALL :RUN_ESPTOOL !RESET_BAUD! --after no_reset read_flash_status
|
||||
GOTO eof
|
||||
)
|
||||
|
||||
@REM Flashing operations.
|
||||
CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET 0x10000..."
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x10000 "!FILENAME!" || GOTO eof
|
||||
CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET !UPDATE_OFFSET!..."
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !UPDATE_OFFSET! "!FILENAME!" || GOTO eof
|
||||
|
||||
CALL :LOG_MESSAGE INFO "Script complete!."
|
||||
|
||||
@@ -145,9 +146,9 @@ EXIT /B %ERRORLEVEL%
|
||||
:RUN_ESPTOOL
|
||||
@REM Subroutine used to run ESPTOOL_CMD with arguments.
|
||||
@REM Also handles %ERRORLEVEL%.
|
||||
@REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write_flash] [OFFSET] [Filename]
|
||||
@REM CALL :RUN_ESPTOOL [Baud] [erase-flash|write-flash] [OFFSET] [Filename]
|
||||
@REM.
|
||||
@REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin"
|
||||
@REM Example:: CALL :RUN_ESPTOOL 115200 write-flash 0x10000 "firmwarefile.bin"
|
||||
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||
CALL :RESET_ERROR
|
||||
!ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
PYTHON=${PYTHON:-$(which python3 python|head -n 1)}
|
||||
CHANGE_MODE=false
|
||||
|
||||
# Constants
|
||||
FLASH_BAUD=115200
|
||||
RESET_BAUD=1200
|
||||
UPDATE_OFFSET=0x10000
|
||||
|
||||
# Determine the correct esptool command to use
|
||||
if "$PYTHON" -m esptool version >/dev/null 2>&1; then
|
||||
ESPTOOL_CMD="$PYTHON -m esptool"
|
||||
@@ -64,7 +69,7 @@ done
|
||||
shift "$((OPTIND-1))"
|
||||
|
||||
if [ "$CHANGE_MODE" = true ]; then
|
||||
$ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status
|
||||
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -75,7 +80,7 @@ fi
|
||||
|
||||
if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then
|
||||
echo "Trying to flash update ${FILENAME}"
|
||||
$ESPTOOL_CMD --baud 115200 write_flash 0x10000 "${FILENAME}"
|
||||
$ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}"
|
||||
else
|
||||
show_help
|
||||
echo "Invalid file: ${FILENAME}"
|
||||
|
||||
@@ -2,50 +2,71 @@
|
||||
|
||||
"""Generate the CI matrix."""
|
||||
|
||||
import configparser
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import random
|
||||
|
||||
rootdir = "variants/"
|
||||
import re
|
||||
from platformio.project.config import ProjectConfig
|
||||
|
||||
options = sys.argv[1:]
|
||||
|
||||
outlist = []
|
||||
|
||||
if len(options) < 1:
|
||||
print(json.dumps(outlist))
|
||||
exit()
|
||||
print(json.dumps(outlist))
|
||||
exit(1)
|
||||
|
||||
for subdir, dirs, files in os.walk(rootdir):
|
||||
for file in files:
|
||||
if file == "platformio.ini":
|
||||
config = configparser.ConfigParser()
|
||||
config.read(subdir + "/" + file)
|
||||
for c in config.sections():
|
||||
if c.startswith("env:"):
|
||||
section = config[c].name[4:]
|
||||
if "extends" in config[config[c].name]:
|
||||
if options[0] + "_base" in config[config[c].name]["extends"]:
|
||||
if "board_level" in config[config[c].name]:
|
||||
if (
|
||||
config[config[c].name]["board_level"] == "extra"
|
||||
) & ("extra" in options):
|
||||
outlist.append(section)
|
||||
else:
|
||||
outlist.append(section)
|
||||
# Add the TFT variants if the base variant is selected
|
||||
elif section.replace("-tft", "") in outlist and config[config[c].name].get("board_level") != "extra":
|
||||
outlist.append(section)
|
||||
elif section.replace("-inkhud", "") in outlist and config[config[c].name].get("board_level") != "extra":
|
||||
outlist.append(section)
|
||||
if "board_check" in config[config[c].name]:
|
||||
if (config[config[c].name]["board_check"] == "true") & (
|
||||
"check" in options
|
||||
):
|
||||
outlist.append(section)
|
||||
if ("quick" in options) & (len(outlist) > 3):
|
||||
print(json.dumps(random.sample(outlist, 3)))
|
||||
cfg = ProjectConfig.get_instance()
|
||||
pio_envs = cfg.envs()
|
||||
|
||||
# Gather all PlatformIO environments for filtering later
|
||||
all_envs = []
|
||||
for pio_env in pio_envs:
|
||||
env_build_flags = cfg.get(f"env:{pio_env}", 'build_flags')
|
||||
env_platform = None
|
||||
for flag in env_build_flags:
|
||||
# Extract the platform from the build flags
|
||||
# Example flag: -I variants/esp32s3/heltec-v3
|
||||
match = re.search(r"-I\s?variants/([^/]+)", flag)
|
||||
if match:
|
||||
env_platform = match.group(1)
|
||||
break
|
||||
# Intentionally fail if platform cannot be determined
|
||||
if not env_platform:
|
||||
print(f"Error: Could not determine platform for environment '{pio_env}'")
|
||||
exit(1)
|
||||
# Store env details as a dictionary, and add to 'all_envs' list
|
||||
env = {
|
||||
'name': pio_env,
|
||||
'platform': env_platform,
|
||||
'board_level': cfg.get(f"env:{pio_env}", 'board_level', default=None),
|
||||
'board_check': bool(cfg.get(f"env:{pio_env}", 'board_check', default=False))
|
||||
}
|
||||
all_envs.append(env)
|
||||
|
||||
# Filter outputs based on options
|
||||
# Check is mutually exclusive with other options (except 'pr')
|
||||
if "check" in options:
|
||||
for env in all_envs:
|
||||
if env['board_check']:
|
||||
if "pr" in options:
|
||||
if env['board_level'] == 'pr':
|
||||
outlist.append(env['name'])
|
||||
else:
|
||||
outlist.append(env['name'])
|
||||
# Filter (non-check) builds by platform
|
||||
else:
|
||||
print(json.dumps(outlist))
|
||||
for env in all_envs:
|
||||
if options[0] == env['platform']:
|
||||
# Always include board_level = 'pr'
|
||||
if env['board_level'] == 'pr':
|
||||
outlist.append(env['name'])
|
||||
# Include board_level = 'extra' when requested
|
||||
elif "extra" in options and env['board_level'] == "extra":
|
||||
outlist.append(env['name'])
|
||||
# If no board level is specified, include in release builds (not PR)
|
||||
elif "pr" not in options and not env['board_level']:
|
||||
outlist.append(env['name'])
|
||||
|
||||
# Return as a JSON list
|
||||
print(json.dumps(outlist))
|
||||
|
||||
@@ -87,6 +87,21 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="2.7.9" date="2025-09-03">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.9</url>
|
||||
</release>
|
||||
<release version="2.7.8" date="2025-08-30">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.8</url>
|
||||
</release>
|
||||
<release version="2.7.7" date="2025-08-28">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.7</url>
|
||||
</release>
|
||||
<release version="2.7.6" date="2025-08-12">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.6</url>
|
||||
</release>
|
||||
<release version="2.7.5" date="2025-08-09">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.5</url>
|
||||
</release>
|
||||
<release version="2.7.4" date="2025-07-19">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.4</url>
|
||||
</release>
|
||||
|
||||
54
boards/heltec_mesh_solar.json
Normal file
54
boards/heltec_mesh_solar.json
Normal file
@@ -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"],
|
||||
["0x239A", "0x0071"]
|
||||
],
|
||||
"usb_product": "HT-n5262",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "heltec_mesh_solar",
|
||||
"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 nrf (Adafruit BSP)",
|
||||
"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/project/meshsolar/",
|
||||
"vendor": "Heltec"
|
||||
}
|
||||
52
boards/meshtiny.json
Normal file
52
boards/meshtiny.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v6.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [
|
||||
["0x239A", "0x8029"],
|
||||
["0x239A", "0x0029"],
|
||||
["0x239A", "0x002A"],
|
||||
["0x239A", "0x802A"]
|
||||
],
|
||||
"usb_product": "MeshTiny",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "meshtiny",
|
||||
"bsp": {
|
||||
"name": "adafruit"
|
||||
},
|
||||
"softdevice": {
|
||||
"sd_flags": "-DS140",
|
||||
"sd_name": "s140",
|
||||
"sd_version": "6.1.1",
|
||||
"sd_fwid": "0x00B6"
|
||||
},
|
||||
"bootloader": {
|
||||
"settings_addr": "0xFF000"
|
||||
}
|
||||
},
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino", "freertos"],
|
||||
"name": "MeshTiny",
|
||||
"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://github.com/meshtastic/firmware",
|
||||
"vendor": "MTools Tec"
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default_8MB.csv",
|
||||
"partitions": "partition-table-8MB.csv",
|
||||
"memory_type": "qio_opi"
|
||||
},
|
||||
"core": "esp32",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"memory_type": "qio_opi",
|
||||
"partitions": "default_8MB.csv"
|
||||
"partitions": "partition-table-8MB.csv"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"core": "stm32",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_GENERIC_WLE5CCUX",
|
||||
"extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_RAK3172_MODULE",
|
||||
"f_cpu": "48000000L",
|
||||
"mcu": "stm32wle5ccu",
|
||||
"variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U",
|
||||
|
||||
17
debian/changelog
vendored
17
debian/changelog
vendored
@@ -1,4 +1,4 @@
|
||||
meshtasticd (2.7.4.0) UNRELEASED; urgency=medium
|
||||
meshtasticd (2.7.9.0) UNRELEASED; urgency=medium
|
||||
|
||||
[ Austin Lane ]
|
||||
* Initial packaging
|
||||
@@ -34,4 +34,17 @@ meshtasticd (2.7.4.0) UNRELEASED; urgency=medium
|
||||
[ ]
|
||||
* GitHub Actions Automatic version bump
|
||||
|
||||
-- <github-actions[bot]@users.noreply.github.com> Sat, 19 Jul 2025 11:36:55 +0000
|
||||
[ ]
|
||||
* GitHub Actions Automatic version bump
|
||||
|
||||
[ ]
|
||||
* GitHub Actions Automatic version bump
|
||||
* GitHub Actions Automatic version bump
|
||||
|
||||
[ ]
|
||||
* GitHub Actions Automatic version bump
|
||||
|
||||
[ ]
|
||||
* GitHub Actions Automatic version bump
|
||||
|
||||
-- <github-actions[bot]@users.noreply.github.com> Wed, 03 Sep 2025 23:39:17 +0000
|
||||
|
||||
2
debian/meshtasticd.postinst
vendored
2
debian/meshtasticd.postinst
vendored
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
# postinst script for meshtasticd
|
||||
#
|
||||
# see: dh_installdeb(1)
|
||||
|
||||
2
debian/meshtasticd.postrm
vendored
2
debian/meshtasticd.postrm
vendored
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
# postrm script for meshtasticd
|
||||
#
|
||||
# see: dh_installdeb(1)
|
||||
|
||||
7
partition-table-8MB.csv
Normal file
7
partition-table-8MB.csv
Normal file
@@ -0,0 +1,7 @@
|
||||
# This is a layout for 8MB of flash for MUI devices
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x5C0000,
|
||||
flashApp, app, ota_1, 0x5D0000,0x0A0000,
|
||||
spiffs, data, spiffs, 0x670000,0x180000
|
||||
|
@@ -60,9 +60,9 @@ monitor_speed = 115200
|
||||
monitor_filters = direct
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/0119501e9983bd894830b02f545c377ee08d66fe.zip
|
||||
# renovate: datasource=custom.pio depName=OneButton packageName=mathertel/library/OneButton
|
||||
mathertel/OneButton@2.6.1
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/9573abb64dc9c94f3051348f2bf4fc5cedf03c22.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
||||
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
||||
https://github.com/meshtastic/arduino-fsm/archive/7db3702bf0cfe97b783d6c72595e3f38e0b19159.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-TinyGPSPlus packageName=https://github.com/meshtastic/TinyGPSPlus gitBranch=master
|
||||
@@ -102,6 +102,14 @@ lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog
|
||||
arcao/Syslog@2.0.0
|
||||
|
||||
; Minimal networking libs for nrf52 (excludes Syslog to save flash)
|
||||
[nrf52_networking_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=custom.pio depName=TBPubSubClient packageName=thingsboard/library/TBPubSubClient
|
||||
thingsboard/TBPubSubClient@2.12.1
|
||||
# renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
|
||||
arduino-libraries/NTPClient@3.2.1
|
||||
|
||||
[radiolib_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
|
||||
@@ -110,7 +118,7 @@ lib_deps =
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||
https://github.com/meshtastic/device-ui/archive/86a09a7360f92d10053fbbf8d74f67f85b0ceb09.zip
|
||||
https://github.com/meshtastic/device-ui/archive/233d18ef42e9d189f90fdfe621f0cd7edff2d221.zip
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
[environmental_base]
|
||||
@@ -149,8 +157,8 @@ lib_deps =
|
||||
emotibit/EmotiBit MLX90632@1.0.8
|
||||
# renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library
|
||||
adafruit/Adafruit MLX90614 Library@2.1.5
|
||||
# renovate: datasource=github-tags depName=INA3221 packageName=KodinLanewave/INA3221
|
||||
https://github.com/KodinLanewave/INA3221/archive/1.0.1.zip
|
||||
# renovate: datasource=github-tags depName=INA3221 packageName=sgtwilko/INA3221
|
||||
https://github.com/sgtwilko/INA3221#bb03d7e9bfcc74fc798838a54f4f99738f29fc6a
|
||||
# renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass
|
||||
mprograms/QMC5883LCompass@1.2.3
|
||||
# renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU
|
||||
@@ -169,6 +177,8 @@ lib_deps =
|
||||
adafruit/Adafruit PCT2075@1.0.5
|
||||
# renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150
|
||||
dfrobot/DFRobot_BMM150@1.0.0
|
||||
# renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561
|
||||
adafruit/Adafruit TSL2561@1.1.2
|
||||
|
||||
; (not included in native / portduino)
|
||||
[environmental_extra]
|
||||
@@ -178,7 +188,7 @@ lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
|
||||
adafruit/Adafruit MAX1704X@1.0.3
|
||||
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
|
||||
adafruit/Adafruit SHTC3 Library@1.0.1
|
||||
adafruit/Adafruit SHTC3 Library@1.0.2
|
||||
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
|
||||
adafruit/Adafruit LPS2X@2.0.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library
|
||||
|
||||
Submodule protobufs updated: fa02e14d8d...a84657c220
@@ -8,6 +8,7 @@
|
||||
"replacements:all",
|
||||
"workarounds:all"
|
||||
],
|
||||
"baseBranchPatterns": ["master"],
|
||||
"forkProcessing": "enabled",
|
||||
"ignoreDeps": [
|
||||
"protobufs"
|
||||
|
||||
@@ -89,14 +89,22 @@ class BluetoothStatus : public Status
|
||||
case ConnectionState::CONNECTED:
|
||||
LOG_DEBUG("BluetoothStatus CONNECTED");
|
||||
#ifdef BLE_LED
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#else
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#endif
|
||||
#endif
|
||||
break;
|
||||
|
||||
case ConnectionState::DISCONNECTED:
|
||||
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
||||
#ifdef BLE_LED
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#else
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#endif
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess
|
||||
{
|
||||
int result;
|
||||
#ifdef ARCH_PORTDUINO
|
||||
bool utf = !settingsMap[ascii_logs];
|
||||
bool utf = !portduino_config.ascii_logs;
|
||||
#else
|
||||
bool utf = true;
|
||||
#endif
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
#include "DisplayFormatters.h"
|
||||
|
||||
const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName)
|
||||
const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName,
|
||||
bool usePreset)
|
||||
{
|
||||
|
||||
// If use_preset is false, always return "Custom"
|
||||
if (!usePreset) {
|
||||
return "Custom";
|
||||
}
|
||||
|
||||
switch (preset) {
|
||||
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
|
||||
return useShortName ? "ShortT" : "ShortTurbo";
|
||||
@@ -31,4 +38,46 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
|
||||
return useShortName ? "Custom" : "Invalid";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role role)
|
||||
{
|
||||
switch (role) {
|
||||
case meshtastic_Config_DeviceConfig_Role_CLIENT:
|
||||
return "Client";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE:
|
||||
return "Client Mute";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN:
|
||||
return "Client Hidden";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND:
|
||||
return "Lost and Found";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_TRACKER:
|
||||
return "Tracker";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_SENSOR:
|
||||
return "Sensor";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_TAK:
|
||||
return "TAK";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER:
|
||||
return "TAK Tracker";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_ROUTER:
|
||||
return "Router";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_ROUTER_LATE:
|
||||
return "Router Late";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_REPEATER:
|
||||
return "Repeater";
|
||||
break;
|
||||
default:
|
||||
return "Unknown";
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,9 @@
|
||||
class DisplayFormatters
|
||||
{
|
||||
public:
|
||||
static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName);
|
||||
static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName,
|
||||
bool usePreset);
|
||||
|
||||
public:
|
||||
static const char *getDeviceRole(meshtastic_Config_DeviceConfig_Role role);
|
||||
};
|
||||
|
||||
@@ -22,6 +22,9 @@ class GPSStatus : public Status
|
||||
|
||||
meshtastic_Position p = meshtastic_Position_init_default;
|
||||
|
||||
/// Time of last valid GPS fix (millis since boot)
|
||||
uint32_t lastFixMillis = 0;
|
||||
|
||||
public:
|
||||
GPSStatus() { statusType = STATUS_TYPE_GPS; }
|
||||
|
||||
@@ -83,6 +86,9 @@ class GPSStatus : public Status
|
||||
|
||||
uint32_t getNumSatellites() const { return p.sats_in_view; }
|
||||
|
||||
/// Return millis() when the last GPS fix occurred (0 = never)
|
||||
uint32_t getLastFixMillis() const { return lastFixMillis; }
|
||||
|
||||
bool matches(const GPSStatus *newStatus) const
|
||||
{
|
||||
#ifdef GPS_DEBUG
|
||||
@@ -114,6 +120,9 @@ class GPSStatus : public Status
|
||||
|
||||
if (isDirty) {
|
||||
if (hasLock) {
|
||||
// Record time of last valid GPS fix
|
||||
lastFixMillis = millis();
|
||||
|
||||
// In debug logs, identify position by @timestamp:stage (stage 3 = notify)
|
||||
LOG_DEBUG("New GPS pos@%x:3 lat=%f lon=%f alt=%d pdop=%.2f track=%.2f speed=%.2f sats=%d", p.timestamp,
|
||||
p.latitude_i * 1e-7, p.longitude_i * 1e-7, p.altitude, p.PDOP * 1e-2, p.ground_track * 1e-5,
|
||||
|
||||
155
src/Power.cpp
155
src/Power.cpp
@@ -128,6 +128,7 @@ RAK9154Sensor rak9154Sensor;
|
||||
#ifdef HAS_PPM
|
||||
// note: XPOWERS_CHIP_XXX must be defined in variant.h
|
||||
#include <XPowersLib.h>
|
||||
XPowersPPM *PPM = NULL;
|
||||
#endif
|
||||
|
||||
#ifdef HAS_BQ27220
|
||||
@@ -681,6 +682,8 @@ bool Power::setup()
|
||||
found = true;
|
||||
} else if (lipoChargerInit()) {
|
||||
found = true;
|
||||
} else if (meshSolarInit()) {
|
||||
found = true;
|
||||
} else if (analogInit()) {
|
||||
found = true;
|
||||
}
|
||||
@@ -724,10 +727,12 @@ void Power::reboot()
|
||||
SPI.end();
|
||||
Wire.end();
|
||||
Serial1.end();
|
||||
if (screen)
|
||||
if (screen) {
|
||||
delete screen;
|
||||
screen = nullptr;
|
||||
}
|
||||
LOG_DEBUG("final reboot!");
|
||||
reboot();
|
||||
::reboot();
|
||||
#elif defined(ARCH_STM32WL)
|
||||
HAL_NVIC_SystemReset();
|
||||
#else
|
||||
@@ -741,7 +746,11 @@ void Power::shutdown()
|
||||
|
||||
#if HAS_SCREEN
|
||||
if (screen) {
|
||||
#ifdef T_DECK_PRO
|
||||
screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button
|
||||
#else
|
||||
screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#if !defined(ARCH_STM32WL)
|
||||
@@ -759,7 +768,7 @@ void Power::shutdown()
|
||||
#ifdef PIN_LED3
|
||||
ledOff(PIN_LED3);
|
||||
#endif
|
||||
doDeepSleep(DELAY_FOREVER, false, true);
|
||||
doDeepSleep(DELAY_FOREVER, true, true);
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
exit(EXIT_SUCCESS);
|
||||
#else
|
||||
@@ -824,16 +833,25 @@ void Power::readPowerStatus()
|
||||
newStatus.notifyObservers(&powerStatus2);
|
||||
#ifdef DEBUG_HEAP
|
||||
if (lastheap != memGet.getFreeHeap()) {
|
||||
std::string threadlist = "Threads running:";
|
||||
// Use stack-allocated buffer to avoid heap allocations in monitoring code
|
||||
char threadlist[256] = "Threads running:";
|
||||
int threadlistLen = strlen(threadlist);
|
||||
int running = 0;
|
||||
for (int i = 0; i < MAX_THREADS; i++) {
|
||||
auto thread = concurrency::mainController.get(i);
|
||||
if ((thread != nullptr) && (thread->enabled)) {
|
||||
threadlist += vformat(" %s", thread->ThreadName.c_str());
|
||||
// Use snprintf to safely append to stack buffer without heap allocation
|
||||
int remaining = sizeof(threadlist) - threadlistLen - 1;
|
||||
if (remaining > 0) {
|
||||
int written = snprintf(threadlist + threadlistLen, remaining, " %s", thread->ThreadName.c_str());
|
||||
if (written > 0 && written < remaining) {
|
||||
threadlistLen += written;
|
||||
}
|
||||
}
|
||||
running++;
|
||||
}
|
||||
}
|
||||
LOG_DEBUG(threadlist.c_str());
|
||||
LOG_DEBUG(threadlist);
|
||||
LOG_DEBUG("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(),
|
||||
memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false));
|
||||
lastheap = memGet.getFreeHeap();
|
||||
@@ -847,15 +865,19 @@ void Power::readPowerStatus()
|
||||
sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]);
|
||||
|
||||
auto newHeap = memGet.getFreeHeap();
|
||||
std::string heapTopic =
|
||||
(*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/heap/") + std::string(mac);
|
||||
std::string heapString = std::to_string(newHeap);
|
||||
mqtt->pubSub.publish(heapTopic.c_str(), heapString.c_str(), false);
|
||||
// Use stack-allocated buffers to avoid heap allocations in monitoring code
|
||||
char heapTopic[128];
|
||||
snprintf(heapTopic, sizeof(heapTopic), "%s/2/heap/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac);
|
||||
char heapString[16];
|
||||
snprintf(heapString, sizeof(heapString), "%u", newHeap);
|
||||
mqtt->pubSub.publish(heapTopic, heapString, false);
|
||||
|
||||
auto wifiRSSI = WiFi.RSSI();
|
||||
std::string wifiTopic =
|
||||
(*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/wifi/") + std::string(mac);
|
||||
std::string wifiString = std::to_string(wifiRSSI);
|
||||
mqtt->pubSub.publish(wifiTopic.c_str(), wifiString.c_str(), false);
|
||||
char wifiTopic[128];
|
||||
snprintf(wifiTopic, sizeof(wifiTopic), "%s/2/wifi/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac);
|
||||
char wifiString[16];
|
||||
snprintf(wifiString, sizeof(wifiString), "%d", wifiRSSI);
|
||||
mqtt->pubSub.publish(wifiTopic, wifiString, false);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1316,7 +1338,6 @@ bool Power::lipoInit()
|
||||
class LipoCharger : public HasBatteryLevel
|
||||
{
|
||||
private:
|
||||
XPowersPPM *ppm = nullptr;
|
||||
BQ27220 *bq = nullptr;
|
||||
|
||||
public:
|
||||
@@ -1325,41 +1346,41 @@ class LipoCharger : public HasBatteryLevel
|
||||
*/
|
||||
bool runOnce()
|
||||
{
|
||||
if (ppm == nullptr) {
|
||||
ppm = new XPowersPPM;
|
||||
bool result = ppm->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
|
||||
if (PPM == nullptr) {
|
||||
PPM = new XPowersPPM;
|
||||
bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
|
||||
if (result) {
|
||||
LOG_INFO("PPM BQ25896 init succeeded");
|
||||
// Set the minimum operating voltage. Below this voltage, the PPM will protect
|
||||
// ppm->setSysPowerDownVoltage(3100);
|
||||
// PPM->setSysPowerDownVoltage(3100);
|
||||
|
||||
// Set input current limit, default is 500mA
|
||||
// ppm->setInputCurrentLimit(800);
|
||||
// PPM->setInputCurrentLimit(800);
|
||||
|
||||
// Disable current limit pin
|
||||
// ppm->disableCurrentLimitPin();
|
||||
// PPM->disableCurrentLimitPin();
|
||||
|
||||
// Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV
|
||||
ppm->setChargeTargetVoltage(4288);
|
||||
PPM->setChargeTargetVoltage(4288);
|
||||
|
||||
// Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA
|
||||
// ppm->setPrechargeCurr(64);
|
||||
// PPM->setPrechargeCurr(64);
|
||||
|
||||
// The premise is that limit pin is disabled, or it will
|
||||
// only follow the maximum charging current set by limit pin.
|
||||
// Set the charging current , Range:0~5056mA ,step:64mA
|
||||
ppm->setChargerConstantCurr(1024);
|
||||
PPM->setChargerConstantCurr(1024);
|
||||
|
||||
// To obtain voltage data, the ADC must be enabled first
|
||||
ppm->enableMeasure();
|
||||
PPM->enableMeasure();
|
||||
|
||||
// Turn on charging function
|
||||
// If there is no battery connected, do not turn on the charging function
|
||||
ppm->enableCharge();
|
||||
PPM->enableCharge();
|
||||
} else {
|
||||
LOG_WARN("PPM BQ25896 init failed");
|
||||
delete ppm;
|
||||
ppm = nullptr;
|
||||
delete PPM;
|
||||
PPM = nullptr;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1400,23 +1421,23 @@ class LipoCharger : public HasBatteryLevel
|
||||
/**
|
||||
* return true if there is a battery installed in this unit
|
||||
*/
|
||||
virtual bool isBatteryConnect() override { return ppm->getBattVoltage() > 0; }
|
||||
virtual bool isBatteryConnect() override { return PPM->getBattVoltage() > 0; }
|
||||
|
||||
/**
|
||||
* return true if there is an external power source detected
|
||||
*/
|
||||
virtual bool isVbusIn() override { return ppm->getVbusVoltage() > 0; }
|
||||
virtual bool isVbusIn() override { return PPM->getVbusVoltage() > 0; }
|
||||
|
||||
/**
|
||||
* return true if the battery is currently charging
|
||||
*/
|
||||
virtual bool isCharging() override
|
||||
{
|
||||
bool isCharging = ppm->isCharging();
|
||||
bool isCharging = PPM->isCharging();
|
||||
if (isCharging) {
|
||||
LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull());
|
||||
} else {
|
||||
if (!ppm->isVbusIn()) {
|
||||
if (!PPM->isVbusIn()) {
|
||||
LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity());
|
||||
}
|
||||
}
|
||||
@@ -1448,3 +1469,73 @@ bool Power::lipoChargerInit()
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HELTEC_MESH_SOLAR
|
||||
#include "meshSolarApp.h"
|
||||
|
||||
/**
|
||||
* meshSolar class for an SMBUS battery sensor.
|
||||
*/
|
||||
class meshSolarBatteryLevel : public HasBatteryLevel
|
||||
{
|
||||
|
||||
public:
|
||||
/**
|
||||
* Init the I2C meshSolar battery level sensor
|
||||
*/
|
||||
bool runOnce()
|
||||
{
|
||||
meshSolarStart();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Battery state of charge, from 0 to 100 or -1 for unknown
|
||||
*/
|
||||
virtual int getBatteryPercent() override { return meshSolarGetBatteryPercent(); }
|
||||
|
||||
/**
|
||||
* The raw voltage of the battery in millivolts, or NAN if unknown
|
||||
*/
|
||||
virtual uint16_t getBattVoltage() override { return meshSolarGetBattVoltage(); }
|
||||
|
||||
/**
|
||||
* return true if there is a battery installed in this unit
|
||||
*/
|
||||
virtual bool isBatteryConnect() override { return meshSolarIsBatteryConnect(); }
|
||||
|
||||
/**
|
||||
* return true if there is an external power source detected
|
||||
*/
|
||||
virtual bool isVbusIn() override { return meshSolarIsVbusIn(); }
|
||||
|
||||
/**
|
||||
* return true if the battery is currently charging
|
||||
*/
|
||||
virtual bool isCharging() override { return meshSolarIsCharging(); }
|
||||
};
|
||||
|
||||
meshSolarBatteryLevel meshSolarLevel;
|
||||
|
||||
/**
|
||||
* Init the meshSolar battery level sensor
|
||||
*/
|
||||
bool Power::meshSolarInit()
|
||||
{
|
||||
bool result = meshSolarLevel.runOnce();
|
||||
LOG_DEBUG("Power::meshSolarInit mesh solar sensor is %s", result ? "ready" : "not ready yet");
|
||||
if (!result)
|
||||
return false;
|
||||
batteryLevel = &meshSolarLevel;
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
/**
|
||||
* The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel
|
||||
*/
|
||||
bool Power::meshSolarInit()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
@@ -57,7 +57,7 @@ size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_l
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
bool color = !settingsMap[ascii_logs];
|
||||
bool color = !portduino_config.ascii_logs;
|
||||
#else
|
||||
bool color = true;
|
||||
#endif
|
||||
@@ -99,7 +99,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
|
||||
size_t r = 0;
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
bool color = !settingsMap[ascii_logs];
|
||||
bool color = !portduino_config.ascii_logs;
|
||||
#else
|
||||
bool color = true;
|
||||
#endif
|
||||
@@ -288,7 +288,7 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
|
||||
#if ARCH_PORTDUINO
|
||||
// level trace is special, two possible ways to handle it.
|
||||
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
|
||||
if (settingsStrings[traceFilename] != "") {
|
||||
if (portduino_config.traceFilename != "") {
|
||||
va_list arg;
|
||||
va_start(arg, format);
|
||||
try {
|
||||
@@ -297,18 +297,18 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
|
||||
}
|
||||
va_end(arg);
|
||||
}
|
||||
if (settingsMap[logoutputlevel] < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
|
||||
if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
|
||||
delete[] newFormat;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (settingsMap[logoutputlevel] < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
|
||||
if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
|
||||
delete[] newFormat;
|
||||
return;
|
||||
} else if (settingsMap[logoutputlevel] < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) {
|
||||
} else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) {
|
||||
delete[] newFormat;
|
||||
return;
|
||||
} else if (settingsMap[logoutputlevel] < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) {
|
||||
} else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) {
|
||||
delete[] newFormat;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,14 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con
|
||||
|
||||
int32_t SerialConsole::runOnce()
|
||||
{
|
||||
#ifdef HELTEC_MESH_SOLAR
|
||||
//After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module.
|
||||
if(moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port
|
||||
&& moduleConfig.serial.mode==meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)
|
||||
{
|
||||
return 250;
|
||||
}
|
||||
#endif
|
||||
return runOncePart();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,11 +28,14 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
|
||||
case INPUT_BROKER_USER_PRESS:
|
||||
case INPUT_BROKER_ALT_PRESS:
|
||||
case INPUT_BROKER_SELECT:
|
||||
case INPUT_BROKER_SELECT_LONG:
|
||||
playBeep(); // Confirmation feedback
|
||||
break;
|
||||
|
||||
case INPUT_BROKER_UP:
|
||||
case INPUT_BROKER_UP_LONG:
|
||||
case INPUT_BROKER_DOWN:
|
||||
case INPUT_BROKER_DOWN_LONG:
|
||||
case INPUT_BROKER_LEFT:
|
||||
case INPUT_BROKER_RIGHT:
|
||||
playChirp(); // Navigation feedback
|
||||
|
||||
@@ -140,6 +140,10 @@ bool playNextLeadUpNote()
|
||||
playTones(¬e, 1); // Play single note using existing playTones function
|
||||
|
||||
leadUpNoteIndex++;
|
||||
|
||||
if (leadUpNoteIndex >= leadUpNotesCount) {
|
||||
return false; // this was the final note
|
||||
}
|
||||
return true; // Note was played (playTones handles buzzer availability internally)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#ifdef RV3028_RTC
|
||||
#if __has_include("Melopero_RV3028.h")
|
||||
#include "Melopero_RV3028.h"
|
||||
#endif
|
||||
#ifdef PCF8563_RTC
|
||||
#if __has_include("pcf8563.h")
|
||||
#include "pcf8563.h"
|
||||
#endif
|
||||
|
||||
@@ -135,7 +135,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
// -----------------------------------------------------------------------------
|
||||
// OLED & Input
|
||||
// -----------------------------------------------------------------------------
|
||||
#if defined(SEEED_WIO_TRACKER_L1)
|
||||
#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK)
|
||||
#define SSD1306_ADDRESS 0x3D
|
||||
#define USE_SH1106
|
||||
#else
|
||||
@@ -431,6 +431,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define MESHTASTIC_EXCLUDE_SERIAL 1
|
||||
#define MESHTASTIC_EXCLUDE_POWERSTRESS 1
|
||||
#define MESHTASTIC_EXCLUDE_ADMIN 1
|
||||
#define MESHTASTIC_EXCLUDE_AMBIENTLIGHTING 1
|
||||
#endif
|
||||
|
||||
// // Turn off wifi even if HW supports wifi (webserver relies on wifi and is also disabled)
|
||||
|
||||
@@ -79,7 +79,9 @@ class ScanI2C
|
||||
BQ27220,
|
||||
LTR553ALS,
|
||||
BHI260AP,
|
||||
BMM150
|
||||
BMM150,
|
||||
TSL2561,
|
||||
DRV2605
|
||||
} DeviceType;
|
||||
|
||||
// typedef uint8_t DeviceAddress;
|
||||
|
||||
@@ -184,8 +184,13 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
type = RTC_RV3028;
|
||||
logFoundDevice("RV3028", (uint8_t)addr.address);
|
||||
rtc.initI2C(*i2cBus);
|
||||
rtc.writeToRegister(0x35, 0x07); // no Clkout
|
||||
rtc.writeToRegister(0x37, 0xB4);
|
||||
// Update RTC EEPROM settings, if necessary
|
||||
if (rtc.readEEPROMRegister(0x35) != 0x07) {
|
||||
rtc.writeEEPROMRegister(0x35, 0x07); // no Clkout
|
||||
}
|
||||
if (rtc.readEEPROMRegister(0x37) != 0xB4) {
|
||||
rtc.writeEEPROMRegister(0x37, 0xB4);
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
|
||||
@@ -456,7 +461,17 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(TCA9555_ADDR, TCA9555, "TCA9555", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(TSL25911_ADDR, TSL2591, "TSL2591", (uint8_t)addr.address);
|
||||
case TSL25911_ADDR:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x12), 1);
|
||||
if (registerValue == 0x50) {
|
||||
type = TSL2591;
|
||||
logFoundDevice("TSL25911", (uint8_t)addr.address);
|
||||
} else {
|
||||
type = TSL2561;
|
||||
logFoundDevice("TSL2561", (uint8_t)addr.address);
|
||||
}
|
||||
break;
|
||||
|
||||
SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address);
|
||||
@@ -478,8 +493,14 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
type = MLX90614;
|
||||
logFoundDevice("MLX90614", (uint8_t)addr.address);
|
||||
} else {
|
||||
type = MPR121KB;
|
||||
logFoundDevice("MPR121KB", (uint8_t)addr.address);
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // DRV2605_REG_STATUS
|
||||
if (registerValue == 0xe0) {
|
||||
type = DRV2605;
|
||||
logFoundDevice("DRV2605", (uint8_t)addr.address);
|
||||
} else {
|
||||
type = MPR121KB;
|
||||
logFoundDevice("MPR121KB", (uint8_t)addr.address);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
119
src/gps/GPS.cpp
119
src/gps/GPS.cpp
@@ -643,8 +643,16 @@ bool GPS::setup()
|
||||
delay(250);
|
||||
} else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) {
|
||||
|
||||
_serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC
|
||||
|
||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_IN ||
|
||||
config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_NP_865) {
|
||||
_serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC
|
||||
// GPS GLONASS GALILEO BDS QZSS NAVIC
|
||||
// 1 0 1 0 0 1
|
||||
} else {
|
||||
_serial_gps->write("$PAIR066,1,1,1,1,0,0*3A\r\n"); // Enable GPS+GLONASS+GALILEO+BDS
|
||||
// GPS GLONASS GALILEO BDS QZSS NAVIC
|
||||
// 1 1 1 1 0 0
|
||||
}
|
||||
// Configure NMEA (sentences will output once per fix)
|
||||
_serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON
|
||||
_serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF
|
||||
@@ -800,6 +808,14 @@ bool GPS::setup()
|
||||
} else {
|
||||
LOG_INFO("GNSS module configuration saved!");
|
||||
}
|
||||
} else if (gnssModel == GNSS_MODEL_CM121) {
|
||||
// only ask for RMC and GGA
|
||||
// enable GGA
|
||||
_serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n");
|
||||
delay(250);
|
||||
// enable RMC
|
||||
_serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n");
|
||||
delay(250);
|
||||
}
|
||||
didSerialInit = true;
|
||||
}
|
||||
@@ -835,9 +851,6 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime)
|
||||
setPowerPMU(true); // Power (PMU): on
|
||||
writePinStandby(false); // Standby (pin): awake (not standby)
|
||||
setPowerUBLOX(true); // Standby (UBLOX): awake
|
||||
#ifdef GNSS_AIROHA
|
||||
lastFixStartMsec = 0;
|
||||
#endif
|
||||
break;
|
||||
|
||||
case GPS_SOFTSLEEP:
|
||||
@@ -855,9 +868,7 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime)
|
||||
writePinStandby(true); // Standby (pin): asleep (not awake)
|
||||
setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed
|
||||
#ifdef GNSS_AIROHA
|
||||
if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) {
|
||||
digitalWrite(PIN_GPS_EN, LOW);
|
||||
}
|
||||
digitalWrite(PIN_GPS_EN, LOW);
|
||||
#endif
|
||||
break;
|
||||
|
||||
@@ -869,9 +880,7 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime)
|
||||
writePinStandby(true); // Standby (pin): asleep
|
||||
setPowerUBLOX(false, 0); // Standby (UBLOX): asleep, indefinitely
|
||||
#ifdef GNSS_AIROHA
|
||||
if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) {
|
||||
digitalWrite(PIN_GPS_EN, LOW);
|
||||
}
|
||||
digitalWrite(PIN_GPS_EN, LOW);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
@@ -1054,6 +1063,8 @@ void GPS::down()
|
||||
}
|
||||
// If update interval long enough (or softsleep unsupported): hardsleep instead
|
||||
setPowerState(GPS_HARDSLEEP, sleepTime);
|
||||
// Reset the fix quality to 0, since we're off.
|
||||
fixQual = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1113,11 +1124,19 @@ int32_t GPS::runOnce()
|
||||
shouldPublish = true;
|
||||
}
|
||||
|
||||
uint8_t prev_fixQual = fixQual;
|
||||
bool gotLoc = lookForLocation();
|
||||
if (gotLoc && !hasValidLocation) { // declare that we have location ASAP
|
||||
LOG_DEBUG("hasValidLocation RISING EDGE");
|
||||
hasValidLocation = true;
|
||||
shouldPublish = true;
|
||||
// Hold for 20secs after getting a lock to download ephemeris etc
|
||||
fixHoldEnds = millis() + 20000;
|
||||
}
|
||||
|
||||
if (gotLoc && prev_fixQual == 0) { // just got a lock after turning back on.
|
||||
fixHoldEnds = millis() + 20000;
|
||||
shouldPublish = true; // Publish immediately, since next publish is at end of hold
|
||||
}
|
||||
|
||||
bool tooLong = scheduling.searchedTooLong();
|
||||
@@ -1126,8 +1145,7 @@ int32_t GPS::runOnce()
|
||||
|
||||
// Once we get a location we no longer desperately want an update
|
||||
if ((gotLoc && gotTime) || tooLong) {
|
||||
|
||||
if (tooLong) {
|
||||
if (tooLong && !gotLoc) {
|
||||
// we didn't get a location during this ack window, therefore declare loss of lock
|
||||
if (hasValidLocation) {
|
||||
LOG_DEBUG("hasValidLocation FALLING EDGE");
|
||||
@@ -1135,9 +1153,15 @@ int32_t GPS::runOnce()
|
||||
p = meshtastic_Position_init_default;
|
||||
hasValidLocation = false;
|
||||
}
|
||||
|
||||
down();
|
||||
shouldPublish = true; // publish our update for this just finished acquisition window
|
||||
if (millis() > fixHoldEnds) {
|
||||
shouldPublish = true; // publish our update at the end of the lock hold
|
||||
publishUpdate();
|
||||
down();
|
||||
#ifdef GPS_DEBUG
|
||||
} else {
|
||||
LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// If state has changed do a publish
|
||||
@@ -1224,9 +1248,15 @@ GnssModel_t GPS::probe(int serialSpeed)
|
||||
_serial_gps->write("$PUBX,40,GSV,0,0,0,0,0,0*59\r\n");
|
||||
_serial_gps->write("$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n");
|
||||
delay(20);
|
||||
// Close NMEA sequences on CM121
|
||||
_serial_gps->write("$CFGMSG,0,1,0,1*1B\r\n");
|
||||
_serial_gps->write("$CFGMSG,0,2,0,1*18\r\n");
|
||||
_serial_gps->write("$CFGMSG,0,3,0,1*19\r\n");
|
||||
delay(20);
|
||||
|
||||
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A
|
||||
std::vector<ChipInfo> unicore = {{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}};
|
||||
// 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}};
|
||||
PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500);
|
||||
|
||||
std::vector<ChipInfo> atgm = {
|
||||
@@ -1406,7 +1436,7 @@ GPS *GPS::createGps()
|
||||
_en_gpio = PIN_GPS_EN;
|
||||
#endif
|
||||
#ifdef ARCH_PORTDUINO
|
||||
if (!settingsMap[has_gps])
|
||||
if (!portduino_config.has_gps)
|
||||
return nullptr;
|
||||
#endif
|
||||
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all
|
||||
@@ -1496,28 +1526,10 @@ static int32_t toDegInt(RawDegrees d)
|
||||
* Perform any processing that should be done only while the GPS is awake and looking for a fix.
|
||||
* Override this method to check for new locations
|
||||
*
|
||||
* @return true if we've acquired a new location
|
||||
* @return true if we've set a new time
|
||||
*/
|
||||
bool GPS::lookForTime()
|
||||
{
|
||||
|
||||
#ifdef GNSS_AIROHA
|
||||
uint8_t fix = reader.fixQuality();
|
||||
if (fix > 0) {
|
||||
if (lastFixStartMsec > 0) {
|
||||
if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) {
|
||||
return false;
|
||||
} else {
|
||||
clearBuffer();
|
||||
}
|
||||
} else {
|
||||
lastFixStartMsec = millis();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
auto ti = reader.time;
|
||||
auto d = reader.date;
|
||||
if (ti.isValid() && d.isValid()) { // Note: we don't check for updated, because we'll only be called if needed
|
||||
@@ -1534,13 +1546,13 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s
|
||||
t.tm_year = d.year() - 1900;
|
||||
t.tm_isdst = false;
|
||||
if (t.tm_mon > -1) {
|
||||
LOG_DEBUG("NMEA GPS time %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min,
|
||||
t.tm_sec, ti.age());
|
||||
if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultInvalidTime) {
|
||||
// Clear the GPS buffer if we got an invalid time
|
||||
clearBuffer();
|
||||
if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) {
|
||||
LOG_DEBUG("NMEA GPS time set %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour,
|
||||
t.tm_min, t.tm_sec, ti.age());
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
} else
|
||||
@@ -1555,25 +1567,6 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s
|
||||
*/
|
||||
bool GPS::lookForLocation()
|
||||
{
|
||||
#ifdef GNSS_AIROHA
|
||||
if ((config.position.gps_update_interval * 1000) >= (GPS_FIX_HOLD_TIME * 2)) {
|
||||
uint8_t fix = reader.fixQuality();
|
||||
if (fix > 0) {
|
||||
if (lastFixStartMsec > 0) {
|
||||
if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) {
|
||||
return false;
|
||||
} else {
|
||||
clearBuffer();
|
||||
}
|
||||
} else {
|
||||
lastFixStartMsec = millis();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// By default, TinyGPS++ does not parse GPGSA lines, which give us
|
||||
// the 2D/3D fixType (see NMEAGPS.h)
|
||||
// At a minimum, use the fixQuality indicator in GPGGA (FIXME?)
|
||||
|
||||
@@ -31,7 +31,8 @@ typedef enum {
|
||||
GNSS_MODEL_MTK_PA1616S,
|
||||
GNSS_MODEL_AG3335,
|
||||
GNSS_MODEL_AG3352,
|
||||
GNSS_MODEL_LS20031
|
||||
GNSS_MODEL_LS20031,
|
||||
GNSS_MODEL_CM121
|
||||
} GnssModel_t;
|
||||
|
||||
typedef enum {
|
||||
@@ -159,7 +160,7 @@ class GPS : private concurrency::OSThread
|
||||
uint8_t fixType = 0; // fix type from GPGSA
|
||||
#endif
|
||||
|
||||
uint32_t lastWakeStartMsec = 0, lastSleepStartMsec = 0, lastFixStartMsec = 0;
|
||||
uint32_t fixHoldEnds = 0;
|
||||
uint32_t rx_gpio = 0;
|
||||
uint32_t tx_gpio = 0;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only upda
|
||||
* Reads the current date and time from the RTC module and updates the system time.
|
||||
* @return True if the RTC was successfully read and the system time was updated, false otherwise.
|
||||
*/
|
||||
void readFromRTC()
|
||||
RTCSetResult readFromRTC()
|
||||
{
|
||||
struct timeval tv; /* btw settimeofday() is helpful here too*/
|
||||
#ifdef RV3028_RTC
|
||||
@@ -44,15 +44,23 @@ void readFromRTC()
|
||||
t.tm_sec = rtc.getSecond();
|
||||
tv.tv_sec = gm_mktime(&t);
|
||||
tv.tv_usec = 0;
|
||||
|
||||
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv.tv_sec < BUILD_EPOCH) {
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
return RTCSetResultInvalidTime;
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1,
|
||||
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
|
||||
timeStartMsec = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
if (currentQuality == RTCQualityNone) {
|
||||
timeStartMsec = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
currentQuality = RTCQualityDevice;
|
||||
}
|
||||
return RTCSetResultSuccess;
|
||||
}
|
||||
#elif defined(PCF8563_RTC)
|
||||
if (rtc_found.address == PCF8563_RTC) {
|
||||
@@ -75,15 +83,23 @@ void readFromRTC()
|
||||
t.tm_sec = tc.second;
|
||||
tv.tv_sec = gm_mktime(&t);
|
||||
tv.tv_usec = 0;
|
||||
|
||||
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv.tv_sec < BUILD_EPOCH) {
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
return RTCSetResultInvalidTime;
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1,
|
||||
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
|
||||
timeStartMsec = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
if (currentQuality == RTCQualityNone) {
|
||||
timeStartMsec = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
currentQuality = RTCQualityDevice;
|
||||
}
|
||||
return RTCSetResultSuccess;
|
||||
}
|
||||
#else
|
||||
if (!gettimeofday(&tv, NULL)) {
|
||||
@@ -92,8 +108,10 @@ void readFromRTC()
|
||||
LOG_DEBUG("Read RTC time as %ld", printableEpoch);
|
||||
timeStartMsec = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
return RTCSetResultSuccess;
|
||||
}
|
||||
#endif
|
||||
return RTCSetResultNotSet;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,7 +119,7 @@ void readFromRTC()
|
||||
*
|
||||
* @param q The quality of the provided time.
|
||||
* @param tv A pointer to a timeval struct containing the time to potentially set the RTC to.
|
||||
* @return True if the RTC was set, false otherwise.
|
||||
* @return RTCSetResult
|
||||
*
|
||||
* If we haven't yet set our RTC this boot, set it from a GPS derived time
|
||||
*/
|
||||
@@ -112,7 +130,15 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv->tv_sec < BUILD_EPOCH) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
#endif
|
||||
return RTCSetResultInvalidTime;
|
||||
} else if (tv->tv_sec > (BUILD_EPOCH + FORTY_YEARS)) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH,
|
||||
BUILD_EPOCH + FORTY_YEARS);
|
||||
#endif
|
||||
return RTCSetResultInvalidTime;
|
||||
}
|
||||
#endif
|
||||
@@ -230,7 +256,15 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
|
||||
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv.tv_sec < BUILD_EPOCH) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
#endif
|
||||
return RTCSetResultInvalidTime;
|
||||
} else if (tv.tv_sec > (BUILD_EPOCH + FORTY_YEARS)) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH,
|
||||
BUILD_EPOCH + FORTY_YEARS);
|
||||
#endif
|
||||
return RTCSetResultInvalidTime;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -48,10 +48,13 @@ uint32_t getTime(bool local = false);
|
||||
/// Return time since 1970 in secs. If quality is RTCQualityNone return zero
|
||||
uint32_t getValidTime(RTCQuality minQuality, bool local = false);
|
||||
|
||||
void readFromRTC();
|
||||
RTCSetResult readFromRTC();
|
||||
|
||||
time_t gm_mktime(struct tm *tm);
|
||||
|
||||
#define SEC_PER_DAY 86400
|
||||
#define SEC_PER_HOUR 3600
|
||||
#define SEC_PER_MIN 60
|
||||
#ifdef BUILD_EPOCH
|
||||
#define FORTY_YEARS (40UL * 365 * SEC_PER_DAY) // probably time to update your firmware
|
||||
#endif
|
||||
|
||||
@@ -67,20 +67,28 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit)
|
||||
|
||||
// FIXME - only draw bits have changed (use backbuf similar to the other displays)
|
||||
const bool flipped = config.display.flip_screen;
|
||||
// HACK for L1 EInk
|
||||
#if defined(SEEED_WIO_TRACKER_L1_EINK)
|
||||
// For SEEED_WIO_TRACKER_L1_EINK, setRotation(3) is correct but mirrored; flip both axes
|
||||
for (uint32_t y = 0; y < displayHeight; y++) {
|
||||
for (uint32_t x = 0; x < displayWidth; x++) {
|
||||
auto b = buffer[x + (y / 8) * displayWidth];
|
||||
auto isset = b & (1 << (y & 7));
|
||||
adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
}
|
||||
}
|
||||
#else
|
||||
for (uint32_t y = 0; y < displayHeight; y++) {
|
||||
for (uint32_t x = 0; x < displayWidth; x++) {
|
||||
// get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient
|
||||
auto b = buffer[x + (y / 8) * displayWidth];
|
||||
auto isset = b & (1 << (y & 7));
|
||||
|
||||
// Handle flip here, rather than with setRotation(),
|
||||
// Avoids issues when display width is not a multiple of 8
|
||||
if (flipped)
|
||||
adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
else
|
||||
adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Trigger the refresh in GxEPD2
|
||||
LOG_DEBUG("Update E-Paper");
|
||||
@@ -140,17 +148,32 @@ bool EInkDisplay::connect()
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1)
|
||||
#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE)
|
||||
{
|
||||
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1);
|
||||
|
||||
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
|
||||
adafruitDisplay->init();
|
||||
#ifdef ELECROW_ThinkNode_M1
|
||||
#if defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE)
|
||||
adafruitDisplay->setRotation(4);
|
||||
#else
|
||||
adafruitDisplay->setRotation(3);
|
||||
#endif
|
||||
adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight);
|
||||
}
|
||||
#elif defined(ELECROW_ThinkNode_M5)
|
||||
{
|
||||
// Start HSPI
|
||||
hspi = new SPIClass(HSPI);
|
||||
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS
|
||||
|
||||
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi);
|
||||
|
||||
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
|
||||
adafruitDisplay->init();
|
||||
|
||||
adafruitDisplay->setRotation(4);
|
||||
|
||||
adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight);
|
||||
}
|
||||
#elif defined(MESHLINK)
|
||||
@@ -220,7 +243,7 @@ bool EInkDisplay::connect()
|
||||
adafruitDisplay->setRotation(1);
|
||||
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
|
||||
}
|
||||
#elif defined(HELTEC_MESH_POCKET)
|
||||
#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK)
|
||||
{
|
||||
spi1 = &SPI1;
|
||||
spi1->begin();
|
||||
@@ -234,6 +257,7 @@ bool EInkDisplay::connect()
|
||||
// Init GxEPD2
|
||||
adafruitDisplay->init();
|
||||
adafruitDisplay->setRotation(3);
|
||||
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
|
||||
}
|
||||
#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213)
|
||||
|
||||
|
||||
@@ -80,11 +80,11 @@ class EInkDisplay : public OLEDDisplay
|
||||
// If display uses HSPI
|
||||
#if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \
|
||||
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \
|
||||
defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER)
|
||||
defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || defined(ELECROW_ThinkNode_M5)
|
||||
SPIClass *hspi = NULL;
|
||||
#endif
|
||||
|
||||
#if defined(HELTEC_MESH_POCKET)
|
||||
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK)
|
||||
SPIClass *spi1 = NULL;
|
||||
#endif
|
||||
|
||||
|
||||
@@ -149,6 +149,11 @@ void Screen::showSimpleBanner(const char *message, uint32_t durationMs)
|
||||
// Called to trigger a banner with custom message and duration
|
||||
void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
|
||||
{
|
||||
// Don't show overlay banner if virtual keyboard is active
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_EINK
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||
#endif
|
||||
@@ -173,6 +178,11 @@ void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
|
||||
// Called to trigger a banner with custom message and duration
|
||||
void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback)
|
||||
{
|
||||
// Don't show node picker if virtual keyboard is active
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_EINK
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||
#endif
|
||||
@@ -196,6 +206,11 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
|
||||
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits,
|
||||
std::function<void(uint32_t)> bannerCallback)
|
||||
{
|
||||
// Don't show number picker if virtual keyboard is active
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_EINK
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||
#endif
|
||||
@@ -216,6 +231,30 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
ui->update();
|
||||
}
|
||||
|
||||
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback)
|
||||
{
|
||||
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);
|
||||
|
||||
// Start OnScreenKeyboardModule session (non-touch variant)
|
||||
OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback);
|
||||
NotificationRenderer::virtualKeyboard = OnScreenKeyboardModule::instance().getKeyboard();
|
||||
NotificationRenderer::textInputCallback = textCallback;
|
||||
|
||||
// Store the message and set the expiration timestamp (use same pattern as other notifications)
|
||||
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
|
||||
NotificationRenderer::alertBannerMessage[255] = '\0';
|
||||
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::text_input;
|
||||
|
||||
// Set the overlay using the same pattern as other notification types
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
}
|
||||
|
||||
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
uint8_t module_frame;
|
||||
@@ -318,7 +357,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \
|
||||
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS)
|
||||
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)
|
||||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY)
|
||||
@@ -332,7 +371,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
#elif ARCH_PORTDUINO
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
if (settingsMap[displayPanel] != no_screen) {
|
||||
if (portduino_config.displayPanel != no_screen) {
|
||||
LOG_DEBUG("Make TFTDisplay!");
|
||||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
@@ -365,9 +404,6 @@ void Screen::doDeepSleep()
|
||||
{
|
||||
#ifdef USE_EINK
|
||||
setOn(false, graphics::UIRenderer::drawDeepSleepFrame);
|
||||
#ifdef PIN_EINK_EN
|
||||
digitalWrite(PIN_EINK_EN, LOW); // power off backlight
|
||||
#endif
|
||||
#else
|
||||
// Without E-Ink display:
|
||||
setOn(false);
|
||||
@@ -391,6 +427,14 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
dispdev->displayOn();
|
||||
#endif
|
||||
|
||||
#ifdef PIN_EINK_EN
|
||||
if (uiconfig.screen_brightness == 1)
|
||||
digitalWrite(PIN_EINK_EN, HIGH);
|
||||
#elif defined(PCA_PIN_EINK_EN)
|
||||
if (uiconfig.screen_brightness == 1)
|
||||
io.digitalWrite(PCA_PIN_EINK_EN, HIGH);
|
||||
#endif
|
||||
|
||||
#if defined(ST7789_CS) && \
|
||||
!defined(M5STACK) // set display brightness when turning on screens. Just moved function from TFTDisplay to here.
|
||||
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
||||
@@ -420,11 +464,13 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
// eInkScreensaver parameter is usually NULL (default argument), default frame used instead
|
||||
setScreensaverFrames(einkScreensaver);
|
||||
#endif
|
||||
#ifdef ELECROW_ThinkNode_M1
|
||||
if (digitalRead(PIN_EINK_EN) == HIGH) {
|
||||
digitalWrite(PIN_EINK_EN, LOW);
|
||||
}
|
||||
|
||||
#ifdef PIN_EINK_EN
|
||||
digitalWrite(PIN_EINK_EN, LOW);
|
||||
#elif defined(PCA_PIN_EINK_EN)
|
||||
io.digitalWrite(PCA_PIN_EINK_EN, LOW);
|
||||
#endif
|
||||
|
||||
dispdev->displayOff();
|
||||
#ifdef USE_ST7789
|
||||
SPI1.end();
|
||||
@@ -543,7 +589,7 @@ void Screen::setup()
|
||||
#else
|
||||
if (!config.display.flip_screen) {
|
||||
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \
|
||||
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS)
|
||||
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)
|
||||
static_cast<TFTDisplay *>(dispdev)->flipScreenVertically();
|
||||
#elif defined(USE_ST7789)
|
||||
static_cast<ST7789Spi *>(dispdev)->flipScreenVertically();
|
||||
@@ -573,7 +619,7 @@ void Screen::setup()
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
if (settingsMap[touchscreenModule]) {
|
||||
if (portduino_config.touchscreenModule) {
|
||||
touchScreenImpl1 =
|
||||
new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast<TFTDisplay *>(dispdev)->getTouch);
|
||||
touchScreenImpl1->init();
|
||||
@@ -685,7 +731,7 @@ int32_t Screen::runOnce()
|
||||
|
||||
#ifndef DISABLE_WELCOME_UNSET
|
||||
if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
||||
menuHandler::LoraRegionPicker(0);
|
||||
menuHandler::OnboardMessage();
|
||||
}
|
||||
#endif
|
||||
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
|
||||
@@ -706,13 +752,19 @@ int32_t Screen::runOnce()
|
||||
handleSetOn(false);
|
||||
break;
|
||||
case Cmd::ON_PRESS:
|
||||
handleOnPress();
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleOnPress();
|
||||
}
|
||||
break;
|
||||
case Cmd::SHOW_PREV_FRAME:
|
||||
handleShowPrevFrame();
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleShowPrevFrame();
|
||||
}
|
||||
break;
|
||||
case Cmd::SHOW_NEXT_FRAME:
|
||||
handleShowNextFrame();
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleShowNextFrame();
|
||||
}
|
||||
break;
|
||||
case Cmd::START_ALERT_FRAME: {
|
||||
showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away
|
||||
@@ -734,7 +786,9 @@ int32_t Screen::runOnce()
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
case Cmd::STOP_BOOT_SCREEN:
|
||||
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
|
||||
setFrames();
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
setFrames();
|
||||
}
|
||||
break;
|
||||
case Cmd::NOOP:
|
||||
break;
|
||||
@@ -770,6 +824,7 @@ int32_t Screen::runOnce()
|
||||
if (showingNormalScreen) {
|
||||
// standard screen loop handling here
|
||||
if (config.display.auto_screen_carousel_secs > 0 &&
|
||||
NotificationRenderer::current_notification_type != notificationTypeEnum::text_input &&
|
||||
!Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) {
|
||||
|
||||
// If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead
|
||||
@@ -860,6 +915,11 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
|
||||
// Called when a frame should be added / removed, or custom frames should be cleared
|
||||
void Screen::setFrames(FrameFocus focus)
|
||||
{
|
||||
// Block setFrames calls when virtual keyboard is active to prevent overlay interference
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t originalPosition = ui->getUiState()->currentFrame;
|
||||
uint8_t previousFrameCount = framesetInfo.frameCount;
|
||||
FramesetInfo fsi; // Location of specific frames, for applying focus parameter
|
||||
@@ -882,71 +942,91 @@ void Screen::setFrames(FrameFocus focus)
|
||||
}
|
||||
|
||||
#if defined(DISPLAY_CLOCK_FRAME)
|
||||
fsi.positions.clock = numframes;
|
||||
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
|
||||
: graphics::ClockRenderer::drawDigitalClockFrame;
|
||||
indicatorIcons.push_back(digital_icon_clock);
|
||||
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);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Declare this early so it’s available in FOCUS_PRESERVE block
|
||||
bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message);
|
||||
|
||||
fsi.positions.home = numframes;
|
||||
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
|
||||
indicatorIcons.push_back(icon_home);
|
||||
if (!hiddenFrames.home) {
|
||||
fsi.positions.home = numframes;
|
||||
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
|
||||
indicatorIcons.push_back(icon_home);
|
||||
}
|
||||
|
||||
fsi.positions.textMessage = numframes;
|
||||
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
|
||||
indicatorIcons.push_back(icon_mail);
|
||||
|
||||
#ifndef USE_EINK
|
||||
fsi.positions.nodelist = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
|
||||
indicatorIcons.push_back(icon_nodes);
|
||||
if (!hiddenFrames.nodelist) {
|
||||
fsi.positions.nodelist = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
|
||||
indicatorIcons.push_back(icon_nodes);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Show detailed node views only on E-Ink builds
|
||||
#ifdef USE_EINK
|
||||
fsi.positions.nodelist_lastheard = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
|
||||
indicatorIcons.push_back(icon_nodes);
|
||||
|
||||
fsi.positions.nodelist_hopsignal = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
|
||||
indicatorIcons.push_back(icon_signal);
|
||||
|
||||
fsi.positions.nodelist_distance = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
|
||||
indicatorIcons.push_back(icon_distance);
|
||||
if (!hiddenFrames.nodelist_lastheard) {
|
||||
fsi.positions.nodelist_lastheard = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
|
||||
indicatorIcons.push_back(icon_nodes);
|
||||
}
|
||||
if (!hiddenFrames.nodelist_hopsignal) {
|
||||
fsi.positions.nodelist_hopsignal = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
|
||||
indicatorIcons.push_back(icon_signal);
|
||||
}
|
||||
if (!hiddenFrames.nodelist_distance) {
|
||||
fsi.positions.nodelist_distance = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
|
||||
indicatorIcons.push_back(icon_distance);
|
||||
}
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
fsi.positions.nodelist_bearings = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
|
||||
indicatorIcons.push_back(icon_list);
|
||||
|
||||
fsi.positions.gps = numframes;
|
||||
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
|
||||
indicatorIcons.push_back(icon_compass);
|
||||
if (!hiddenFrames.nodelist_bearings) {
|
||||
fsi.positions.nodelist_bearings = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
|
||||
indicatorIcons.push_back(icon_list);
|
||||
}
|
||||
if (!hiddenFrames.gps) {
|
||||
fsi.positions.gps = numframes;
|
||||
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
|
||||
indicatorIcons.push_back(icon_compass);
|
||||
}
|
||||
#endif
|
||||
if (RadioLibInterface::instance) {
|
||||
if (RadioLibInterface::instance && !hiddenFrames.lora) {
|
||||
fsi.positions.lora = numframes;
|
||||
normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused;
|
||||
indicatorIcons.push_back(icon_radio);
|
||||
}
|
||||
if (!dismissedFrames.memory) {
|
||||
fsi.positions.memory = numframes;
|
||||
normalFrames[numframes++] = graphics::DebugRenderer::drawMemoryUsage;
|
||||
indicatorIcons.push_back(icon_memory);
|
||||
if (!hiddenFrames.system) {
|
||||
fsi.positions.system = numframes;
|
||||
normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen;
|
||||
indicatorIcons.push_back(icon_system);
|
||||
}
|
||||
#if !defined(DISPLAY_CLOCK_FRAME)
|
||||
fsi.positions.clock = numframes;
|
||||
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
|
||||
: graphics::ClockRenderer::drawDigitalClockFrame;
|
||||
indicatorIcons.push_back(digital_icon_clock);
|
||||
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);
|
||||
}
|
||||
#endif
|
||||
if (!hiddenFrames.chirpy) {
|
||||
fsi.positions.chirpy = numframes;
|
||||
normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy;
|
||||
indicatorIcons.push_back(small_chirpy);
|
||||
}
|
||||
|
||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||
if (!dismissedFrames.wifi && isWifiAvailable()) {
|
||||
if (!hiddenFrames.wifi && isWifiAvailable()) {
|
||||
fsi.positions.wifi = numframes;
|
||||
normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline;
|
||||
indicatorIcons.push_back(icon_wifi);
|
||||
@@ -988,27 +1068,29 @@ void Screen::setFrames(FrameFocus focus)
|
||||
if (numMeshNodes > 0)
|
||||
numMeshNodes--;
|
||||
|
||||
// Temporary array to hold favorite node frames
|
||||
std::vector<FrameCallback> favoriteFrames;
|
||||
if (!hiddenFrames.show_favorites) {
|
||||
// Temporary array to hold favorite node frames
|
||||
std::vector<FrameCallback> favoriteFrames;
|
||||
|
||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
||||
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
|
||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
||||
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
// 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.lastFavorite = numframes - 1;
|
||||
} else {
|
||||
fsi.positions.firstFavorite = 255;
|
||||
fsi.positions.lastFavorite = 255;
|
||||
}
|
||||
|
||||
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
|
||||
@@ -1047,7 +1129,7 @@ void Screen::setFrames(FrameFocus focus)
|
||||
ui->switchToFrame(fsi.positions.clock);
|
||||
break;
|
||||
case FOCUS_SYSTEM:
|
||||
ui->switchToFrame(fsi.positions.memory);
|
||||
ui->switchToFrame(fsi.positions.system);
|
||||
break;
|
||||
|
||||
case FOCUS_PRESERVE:
|
||||
@@ -1075,30 +1157,101 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames)
|
||||
setFastFramerate();
|
||||
}
|
||||
|
||||
void Screen::toggleFrameVisibility(const std::string &frameName)
|
||||
{
|
||||
#ifndef USE_EINK
|
||||
if (frameName == "nodelist") {
|
||||
hiddenFrames.nodelist = !hiddenFrames.nodelist;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_EINK
|
||||
if (frameName == "nodelist_lastheard") {
|
||||
hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard;
|
||||
}
|
||||
if (frameName == "nodelist_hopsignal") {
|
||||
hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal;
|
||||
}
|
||||
if (frameName == "nodelist_distance") {
|
||||
hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance;
|
||||
}
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
if (frameName == "nodelist_bearings") {
|
||||
hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings;
|
||||
}
|
||||
if (frameName == "gps") {
|
||||
hiddenFrames.gps = !hiddenFrames.gps;
|
||||
}
|
||||
#endif
|
||||
if (frameName == "lora") {
|
||||
hiddenFrames.lora = !hiddenFrames.lora;
|
||||
}
|
||||
if (frameName == "clock") {
|
||||
hiddenFrames.clock = !hiddenFrames.clock;
|
||||
}
|
||||
if (frameName == "show_favorites") {
|
||||
hiddenFrames.show_favorites = !hiddenFrames.show_favorites;
|
||||
}
|
||||
if (frameName == "chirpy") {
|
||||
hiddenFrames.chirpy = !hiddenFrames.chirpy;
|
||||
}
|
||||
}
|
||||
|
||||
bool Screen::isFrameHidden(const std::string &frameName) const
|
||||
{
|
||||
#ifndef USE_EINK
|
||||
if (frameName == "nodelist")
|
||||
return hiddenFrames.nodelist;
|
||||
#endif
|
||||
#ifdef USE_EINK
|
||||
if (frameName == "nodelist_lastheard")
|
||||
return hiddenFrames.nodelist_lastheard;
|
||||
if (frameName == "nodelist_hopsignal")
|
||||
return hiddenFrames.nodelist_hopsignal;
|
||||
if (frameName == "nodelist_distance")
|
||||
return hiddenFrames.nodelist_distance;
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
if (frameName == "nodelist_bearings")
|
||||
return hiddenFrames.nodelist_bearings;
|
||||
if (frameName == "gps")
|
||||
return hiddenFrames.gps;
|
||||
#endif
|
||||
if (frameName == "lora")
|
||||
return hiddenFrames.lora;
|
||||
if (frameName == "clock")
|
||||
return hiddenFrames.clock;
|
||||
if (frameName == "show_favorites")
|
||||
return hiddenFrames.show_favorites;
|
||||
if (frameName == "chirpy")
|
||||
return hiddenFrames.chirpy;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Dismisses the currently displayed screen frame, if possible
|
||||
// Relevant for text message, waypoint, others in future?
|
||||
// Triggered with a CardKB keycombo
|
||||
void Screen::dismissCurrentFrame()
|
||||
void Screen::hideCurrentFrame()
|
||||
{
|
||||
uint8_t currentFrame = ui->getUiState()->currentFrame;
|
||||
bool dismissed = false;
|
||||
|
||||
if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) {
|
||||
LOG_INFO("Dismiss Text Message");
|
||||
LOG_INFO("Hide Text Message");
|
||||
devicestate.has_rx_text_message = false;
|
||||
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
|
||||
} else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) {
|
||||
LOG_DEBUG("Dismiss Waypoint");
|
||||
LOG_DEBUG("Hide Waypoint");
|
||||
devicestate.has_rx_waypoint = false;
|
||||
dismissedFrames.waypoint = true;
|
||||
hiddenFrames.waypoint = true;
|
||||
dismissed = true;
|
||||
} else if (currentFrame == framesetInfo.positions.wifi) {
|
||||
LOG_DEBUG("Dismiss WiFi Screen");
|
||||
dismissedFrames.wifi = true;
|
||||
LOG_DEBUG("Hide WiFi Screen");
|
||||
hiddenFrames.wifi = true;
|
||||
dismissed = true;
|
||||
} else if (currentFrame == framesetInfo.positions.memory) {
|
||||
LOG_INFO("Dismiss Memory");
|
||||
dismissedFrames.memory = true;
|
||||
} else if (currentFrame == framesetInfo.positions.lora) {
|
||||
LOG_INFO("Hide LoRa");
|
||||
hiddenFrames.lora = true;
|
||||
dismissed = true;
|
||||
}
|
||||
|
||||
@@ -1250,7 +1403,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
// Outgoing message (likely sent from phone)
|
||||
devicestate.has_rx_text_message = false;
|
||||
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
|
||||
dismissedFrames.textMessage = true;
|
||||
hiddenFrames.textMessage = true;
|
||||
hasUnreadMessage = false; // Clear unread state when user replies
|
||||
|
||||
setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list
|
||||
@@ -1258,30 +1411,40 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
// Incoming message
|
||||
devicestate.has_rx_text_message = true; // Needed to include the message frame
|
||||
hasUnreadMessage = true; // Enables mail icon in the header
|
||||
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view
|
||||
|
||||
// Only wake/force display if the configuration allows it
|
||||
if (shouldWakeOnReceivedMessage()) {
|
||||
setOn(true); // Wake up the screen first
|
||||
forceDisplay(); // Forces screen redraw
|
||||
// Always update frame list to include new message, but defer UI updates if virtual keyboard is active
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view
|
||||
|
||||
// === Prepare banner content ===
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
|
||||
const char *longName = (node && node->has_user) ? node->user.long_name : nullptr;
|
||||
|
||||
const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
|
||||
|
||||
char banner[256];
|
||||
|
||||
// Check for bell character in message to determine alert type
|
||||
bool isAlert = false;
|
||||
for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) {
|
||||
if (msgRaw[i] == '\x07') {
|
||||
isAlert = true;
|
||||
break;
|
||||
}
|
||||
// Only wake/force display if the configuration allows it
|
||||
if (shouldWakeOnReceivedMessage()) {
|
||||
setOn(true); // Wake up the screen first
|
||||
forceDisplay(); // Forces screen redraw
|
||||
}
|
||||
} else {
|
||||
// Virtual keyboard is active - just mark that frames need regeneration when keyboard closes
|
||||
// The devicestate and hasUnreadMessage are already set above, so message will appear later
|
||||
LOG_DEBUG("Virtual keyboard active - deferring frame list update for new message");
|
||||
}
|
||||
|
||||
// Show message alert - either as normal banner or as keyboard popup
|
||||
// === Common variables ===
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
|
||||
const char *longName = (node && node->has_user) ? node->user.long_name : nullptr;
|
||||
const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
|
||||
|
||||
// Check for bell character in message to determine alert type
|
||||
bool isAlert = false;
|
||||
for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) {
|
||||
if (msgRaw[i] == '\x07') {
|
||||
isAlert = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
// === Normal banner mode ===
|
||||
char banner[256];
|
||||
if (isAlert) {
|
||||
if (longName && longName[0]) {
|
||||
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
|
||||
@@ -1295,8 +1458,40 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
strcpy(banner, "New Message");
|
||||
}
|
||||
}
|
||||
|
||||
screen->showSimpleBanner(banner, 3000);
|
||||
} else {
|
||||
// === Virtual keyboard popup mode ===
|
||||
char title[64];
|
||||
if (isAlert) {
|
||||
if (longName && longName[0]) {
|
||||
snprintf(title, sizeof(title), "Alert from %s", longName);
|
||||
} else {
|
||||
strcpy(title, "Alert Received");
|
||||
}
|
||||
} else {
|
||||
if (longName && longName[0]) {
|
||||
snprintf(title, sizeof(title), "%s", longName);
|
||||
} else {
|
||||
strcpy(title, "New Message");
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare content - clean the message content
|
||||
char content[200];
|
||||
size_t contentLen = 0;
|
||||
for (size_t i = 0; i < packet->decoded.payload.size && i < sizeof(content) - 1; i++) {
|
||||
if (msgRaw[i] != '\x07' && msgRaw[i] != '\0') { // Skip bell character and null
|
||||
content[contentLen++] = msgRaw[i];
|
||||
}
|
||||
}
|
||||
content[contentLen] = '\0';
|
||||
|
||||
// Show popup with title and content on virtual keyboard
|
||||
NotificationRenderer::showKeyboardMessagePopupWithTitle(title, content, 5000);
|
||||
|
||||
// Force display update to show the popup immediately
|
||||
setFastFramerate();
|
||||
forceDisplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1307,6 +1502,11 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
// Triggered by MeshModules
|
||||
int Screen::handleUIFrameEvent(const UIFrameEvent *event)
|
||||
{
|
||||
// Block UI frame events when virtual keyboard is active
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (showingNormalScreen) {
|
||||
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
|
||||
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
|
||||
@@ -1329,6 +1529,16 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (!screenOn)
|
||||
return 0;
|
||||
|
||||
// Handle text input notifications specially - pass input to virtual keyboard
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
NotificationRenderer::inEvent = *event;
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
setFastFramerate(); // Draw ASAP
|
||||
ui->update();
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw.
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please
|
||||
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update
|
||||
@@ -1366,7 +1576,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
||||
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
||||
menuHandler::homeBaseMenu();
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) {
|
||||
menuHandler::systemBaseMenu();
|
||||
#if HAS_GPS
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) {
|
||||
@@ -1375,7 +1585,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) {
|
||||
menuHandler::clockMenu();
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
|
||||
menuHandler::LoraRegionPicker();
|
||||
menuHandler::loraMenu();
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
||||
if (devicestate.rx_text_message.from) {
|
||||
menuHandler::messageResponseMenu();
|
||||
|
||||
@@ -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 };
|
||||
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input };
|
||||
|
||||
struct BannerOverlayOptions {
|
||||
const char *message;
|
||||
@@ -313,6 +313,8 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
|
||||
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback);
|
||||
|
||||
void requestMenu(graphics::menuHandler::screenMenus menuToShow)
|
||||
{
|
||||
@@ -591,7 +593,11 @@ class Screen : public concurrency::OSThread
|
||||
void setSSLFrames();
|
||||
|
||||
// Dismiss the currently focussed frame, if possible (e.g. text message, waypoint)
|
||||
void dismissCurrentFrame();
|
||||
void hideCurrentFrame();
|
||||
|
||||
// Menu-driven Show / Hide Toggle
|
||||
void toggleFrameVisibility(const std::string &frameName);
|
||||
bool isFrameHidden(const std::string &frameName) const;
|
||||
|
||||
#ifdef USE_EINK
|
||||
/// Draw an image to remain on E-Ink display after screen off
|
||||
@@ -653,7 +659,7 @@ class Screen : public concurrency::OSThread
|
||||
uint8_t settings = 255;
|
||||
uint8_t wifi = 255;
|
||||
uint8_t deviceFocused = 255;
|
||||
uint8_t memory = 255;
|
||||
uint8_t system = 255;
|
||||
uint8_t gps = 255;
|
||||
uint8_t home = 255;
|
||||
uint8_t textMessage = 255;
|
||||
@@ -663,6 +669,7 @@ class Screen : public concurrency::OSThread
|
||||
uint8_t nodelist_distance = 255;
|
||||
uint8_t nodelist_bearings = 255;
|
||||
uint8_t clock = 255;
|
||||
uint8_t chirpy = 255;
|
||||
uint8_t firstFavorite = 255;
|
||||
uint8_t lastFavorite = 255;
|
||||
uint8_t lora = 255;
|
||||
@@ -671,12 +678,29 @@ class Screen : public concurrency::OSThread
|
||||
uint8_t frameCount = 0;
|
||||
} framesetInfo;
|
||||
|
||||
struct DismissedFrames {
|
||||
struct hiddenFrames {
|
||||
bool textMessage = false;
|
||||
bool waypoint = false;
|
||||
bool wifi = false;
|
||||
bool memory = false;
|
||||
} dismissedFrames;
|
||||
bool system = false;
|
||||
bool home = false;
|
||||
bool clock = false;
|
||||
#ifndef USE_EINK
|
||||
bool nodelist = false;
|
||||
#endif
|
||||
#ifdef USE_EINK
|
||||
bool nodelist_lastheard = false;
|
||||
bool nodelist_hopsignal = false;
|
||||
bool nodelist_distance = false;
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
bool nodelist_bearings = false;
|
||||
bool gps = false;
|
||||
#endif
|
||||
bool lora = false;
|
||||
bool show_favorites = false;
|
||||
bool chirpy = true;
|
||||
} hiddenFrames;
|
||||
|
||||
/// Try to start drawing ASAP
|
||||
void setFastFramerate();
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#include "graphics/fonts/OLEDDisplayFontsCS.h"
|
||||
#endif
|
||||
|
||||
#ifdef CROWPANEL_ESP32S3_5_EPAPER
|
||||
#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK)
|
||||
#include "graphics/fonts/EinkDisplayFonts.h"
|
||||
#endif
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
#ifdef OLED_PL
|
||||
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_PL // Height: 19
|
||||
#else
|
||||
#ifdef OLED_RU
|
||||
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_RU // Height: 19
|
||||
#else
|
||||
#ifdef OLED_UA
|
||||
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_UA // Height: 19
|
||||
#else
|
||||
@@ -50,9 +53,13 @@
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#ifdef OLED_PL
|
||||
#define FONT_LARGE_LOCAL ArialMT_Plain_24_PL // Height: 28
|
||||
#else
|
||||
#ifdef OLED_RU
|
||||
#define FONT_LARGE_LOCAL ArialMT_Plain_24_RU // Height: 28
|
||||
#else
|
||||
#ifdef OLED_UA
|
||||
#define FONT_LARGE_LOCAL ArialMT_Plain_24_UA // Height: 28
|
||||
#else
|
||||
@@ -63,9 +70,10 @@
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS)) && \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
// The screen is bigger so use bigger fonts
|
||||
#define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19
|
||||
@@ -77,7 +85,7 @@
|
||||
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28
|
||||
#endif
|
||||
|
||||
#if defined(CROWPANEL_ESP32S3_5_EPAPER)
|
||||
#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK)
|
||||
#undef FONT_SMALL
|
||||
#undef FONT_MEDIUM
|
||||
#undef FONT_LARGE
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#include "main.h"
|
||||
#include "meshtastic/config.pb.h"
|
||||
#include "power.h"
|
||||
@@ -16,6 +17,10 @@ void determineResolution(int16_t screenheight, int16_t screenwidth)
|
||||
isHighResolution = true;
|
||||
}
|
||||
|
||||
if (screenwidth > 128 && screenheight <= 64) {
|
||||
isHighResolution = false;
|
||||
}
|
||||
|
||||
// Special case for Heltec Wireless Tracker v1.1
|
||||
if (screenwidth == 160 && screenheight == 80) {
|
||||
isHighResolution = false;
|
||||
@@ -53,7 +58,7 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
|
||||
// *************************
|
||||
// * Common Header Drawing *
|
||||
// *************************
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only)
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date)
|
||||
{
|
||||
constexpr int HEADER_OFFSET_Y = 1;
|
||||
y += HEADER_OFFSET_Y;
|
||||
@@ -69,7 +74,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
const int screenW = display->getWidth();
|
||||
const int screenH = display->getHeight();
|
||||
|
||||
if (!battery_only) {
|
||||
if (!force_no_invert) {
|
||||
// === Inverted Header Background ===
|
||||
if (isInverted) {
|
||||
display->setColor(BLACK);
|
||||
@@ -187,13 +192,28 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
int timeStrWidth = display->getStringWidth("12:34"); // Default alignment
|
||||
int timeX = screenW - xOffset - timeStrWidth + 4;
|
||||
|
||||
if (rtc_sec > 0 && !battery_only) {
|
||||
if (rtc_sec > 0) {
|
||||
// === Build Time String ===
|
||||
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
int hour = hms / SEC_PER_HOUR;
|
||||
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
||||
|
||||
// === Build Date String ===
|
||||
char datetimeStr[25];
|
||||
UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false);
|
||||
char dateLine[40];
|
||||
|
||||
if (isHighResolution) {
|
||||
snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr);
|
||||
} else {
|
||||
if (hasUnreadMessage) {
|
||||
snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[5]);
|
||||
} else {
|
||||
snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[2]);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.display.use_12h_clock) {
|
||||
bool isPM = hour >= 12;
|
||||
hour %= 12;
|
||||
@@ -202,7 +222,11 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a");
|
||||
}
|
||||
|
||||
timeStrWidth = display->getStringWidth(timeStr);
|
||||
if (show_date) {
|
||||
timeStrWidth = display->getStringWidth(dateLine);
|
||||
} else {
|
||||
timeStrWidth = display->getStringWidth(timeStr);
|
||||
}
|
||||
timeX = screenW - xOffset - timeStrWidth + 3;
|
||||
|
||||
// === Show Mail or Mute Icon to the Left of Time ===
|
||||
@@ -229,7 +253,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
int iconW = 16, iconH = 12;
|
||||
int iconX = iconRightEdge - iconW;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
||||
if (isInverted) {
|
||||
if (isInverted && !force_no_invert) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
|
||||
display->setColor(BLACK);
|
||||
@@ -244,7 +268,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
} else {
|
||||
int iconX = iconRightEdge - (mail_width - 2);
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||
if (isInverted) {
|
||||
if (isInverted && !force_no_invert) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
|
||||
display->setColor(BLACK);
|
||||
@@ -287,10 +311,17 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
}
|
||||
}
|
||||
|
||||
// === Draw Time ===
|
||||
display->drawString(timeX, textY, timeStr);
|
||||
if (isBold)
|
||||
display->drawString(timeX - 1, textY, timeStr);
|
||||
if (show_date) {
|
||||
// === Draw Date ===
|
||||
display->drawString(timeX, textY, dateLine);
|
||||
if (isBold)
|
||||
display->drawString(timeX - 1, textY, dateLine);
|
||||
} else {
|
||||
// === Draw Time ===
|
||||
display->drawString(timeX, textY, timeStr);
|
||||
if (isBold)
|
||||
display->drawString(timeX - 1, textY, timeStr);
|
||||
}
|
||||
|
||||
} else {
|
||||
// === No Time Available: Mail/Mute Icon Moves to Far Right ===
|
||||
|
||||
@@ -49,7 +49,8 @@ void determineResolution(int16_t screenheight, int16_t screenwidth);
|
||||
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
|
||||
|
||||
// Shared battery/time/mail header
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool battery_only = false);
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false,
|
||||
bool show_date = false);
|
||||
|
||||
const int *getTextPositions(OLEDDisplay *display);
|
||||
|
||||
|
||||
@@ -562,6 +562,91 @@ class LGFX : public lgfx::LGFX_Device
|
||||
|
||||
static LGFX *tft = nullptr;
|
||||
|
||||
#elif defined(ST7796_CS)
|
||||
#include <LovyanGFX.hpp> // Graphics and font library for ST7796 driver chip
|
||||
|
||||
class LGFX : public lgfx::LGFX_Device
|
||||
{
|
||||
lgfx::Panel_ST7796 _panel_instance;
|
||||
lgfx::Bus_SPI _bus_instance;
|
||||
lgfx::Light_PWM _light_instance;
|
||||
|
||||
public:
|
||||
LGFX(void)
|
||||
{
|
||||
{
|
||||
auto cfg = _bus_instance.config();
|
||||
|
||||
// SPI
|
||||
cfg.spi_host = ST7796_SPI_HOST;
|
||||
cfg.spi_mode = 0;
|
||||
cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing
|
||||
// 80MHz by an integer)
|
||||
cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving
|
||||
cfg.spi_3wire = false;
|
||||
cfg.use_lock = true; // Set to true to use transaction locking
|
||||
cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch /
|
||||
// SPI_DMA_CH_AUTO=auto setting)
|
||||
cfg.pin_sclk = ST7796_SCK; // Set SPI SCLK pin number
|
||||
cfg.pin_mosi = ST7796_SDA; // Set SPI MOSI pin number
|
||||
cfg.pin_miso = ST7796_MISO; // Set SPI MISO pin number (-1 = disable)
|
||||
cfg.pin_dc = ST7796_RS; // Set SPI DC pin number (-1 = disable)
|
||||
|
||||
_bus_instance.config(cfg); // applies the set value to the bus.
|
||||
_panel_instance.setBus(&_bus_instance); // set the bus on the panel.
|
||||
}
|
||||
|
||||
{ // Set the display panel control.
|
||||
auto cfg = _panel_instance.config(); // Gets a structure for display panel settings.
|
||||
|
||||
cfg.pin_cs = ST7796_CS; // Pin number where CS is connected (-1 = disable)
|
||||
cfg.pin_rst = ST7796_RESET; // Pin number where RST is connected (-1 = disable)
|
||||
cfg.pin_busy = ST7796_BUSY; // Pin number where BUSY is connected (-1 = disable)
|
||||
|
||||
// cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC
|
||||
// cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC
|
||||
cfg.panel_width = TFT_WIDTH; // actual displayable width
|
||||
cfg.panel_height = TFT_HEIGHT; // actual displayable height
|
||||
cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction
|
||||
cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction
|
||||
cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored)
|
||||
#ifdef TFT_DUMMY_READ_PIXELS
|
||||
cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout
|
||||
#else
|
||||
cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout
|
||||
#endif
|
||||
cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read
|
||||
cfg.readable = true; // Set to true if data can be read
|
||||
cfg.invert = true; // Set to true if the light/darkness of the panel is reversed
|
||||
cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped
|
||||
cfg.dlen_16bit =
|
||||
false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI
|
||||
cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.)
|
||||
|
||||
_panel_instance.config(cfg);
|
||||
}
|
||||
|
||||
#ifdef ST7796_BL
|
||||
// Set the backlight control. (delete if not necessary)
|
||||
{
|
||||
auto cfg = _light_instance.config(); // Gets a structure for backlight settings.
|
||||
|
||||
cfg.pin_bl = ST7796_BL; // Pin number to which the backlight is connected
|
||||
cfg.invert = false; // true to invert the brightness of the backlight
|
||||
cfg.freq = 44100;
|
||||
cfg.pwm_channel = 7;
|
||||
|
||||
_light_instance.config(cfg);
|
||||
_panel_instance.setLight(&_light_instance); // Set the backlight on the panel.
|
||||
}
|
||||
#endif
|
||||
|
||||
setPanel(&_panel_instance); // Sets the panel to use.
|
||||
}
|
||||
};
|
||||
|
||||
static LGFX *tft = nullptr;
|
||||
|
||||
#elif defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER)
|
||||
|
||||
#include <LovyanGFX.hpp> // Graphics and font library for ILI9341/ILI9342 driver chip
|
||||
@@ -667,33 +752,42 @@ static LGFX *tft = nullptr;
|
||||
static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h
|
||||
#elif ARCH_PORTDUINO
|
||||
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
|
||||
#if defined(LGFX_SDL)
|
||||
#include <lgfx/v1/platforms/sdl/Panel_sdl.hpp>
|
||||
#endif
|
||||
|
||||
class LGFX : public lgfx::LGFX_Device
|
||||
{
|
||||
lgfx::Panel_Device *_panel_instance;
|
||||
lgfx::Bus_SPI _bus_instance;
|
||||
|
||||
lgfx::ITouch *_touch_instance;
|
||||
|
||||
public:
|
||||
lgfx::Panel_Device *_panel_instance;
|
||||
|
||||
LGFX(void)
|
||||
{
|
||||
if (settingsMap[displayPanel] == st7789)
|
||||
if (portduino_config.displayPanel == st7789)
|
||||
_panel_instance = new lgfx::Panel_ST7789;
|
||||
else if (settingsMap[displayPanel] == st7735)
|
||||
else if (portduino_config.displayPanel == st7735)
|
||||
_panel_instance = new lgfx::Panel_ST7735;
|
||||
else if (settingsMap[displayPanel] == st7735s)
|
||||
else if (portduino_config.displayPanel == st7735s)
|
||||
_panel_instance = new lgfx::Panel_ST7735S;
|
||||
else if (settingsMap[displayPanel] == st7796)
|
||||
else if (portduino_config.displayPanel == st7796)
|
||||
_panel_instance = new lgfx::Panel_ST7796;
|
||||
else if (settingsMap[displayPanel] == ili9341)
|
||||
else if (portduino_config.displayPanel == ili9341)
|
||||
_panel_instance = new lgfx::Panel_ILI9341;
|
||||
else if (settingsMap[displayPanel] == ili9342)
|
||||
else if (portduino_config.displayPanel == ili9342)
|
||||
_panel_instance = new lgfx::Panel_ILI9342;
|
||||
else if (settingsMap[displayPanel] == ili9488)
|
||||
else if (portduino_config.displayPanel == ili9488)
|
||||
_panel_instance = new lgfx::Panel_ILI9488;
|
||||
else if (settingsMap[displayPanel] == hx8357d)
|
||||
else if (portduino_config.displayPanel == hx8357d)
|
||||
_panel_instance = new lgfx::Panel_HX8357D;
|
||||
#if defined(LGFX_SDL)
|
||||
else if (portduino_config.displayPanel == x11) {
|
||||
_panel_instance = new lgfx::Panel_sdl;
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
_panel_instance = new lgfx::Panel_NULL;
|
||||
LOG_ERROR("Unknown display panel configured!");
|
||||
@@ -701,60 +795,66 @@ class LGFX : public lgfx::LGFX_Device
|
||||
|
||||
auto buscfg = _bus_instance.config();
|
||||
buscfg.spi_mode = 0;
|
||||
buscfg.spi_host = settingsMap[displayspidev];
|
||||
buscfg.spi_host = portduino_config.display_spi_dev_int;
|
||||
|
||||
buscfg.pin_dc = settingsMap[displayDC]; // Set SPI DC pin number (-1 = disable)
|
||||
buscfg.pin_dc = portduino_config.displayDC.pin; // Set SPI DC pin number (-1 = disable)
|
||||
|
||||
_bus_instance.config(buscfg); // applies the set value to the bus.
|
||||
_panel_instance->setBus(&_bus_instance); // set the bus on the panel.
|
||||
|
||||
auto cfg = _panel_instance->config(); // Gets a structure for display panel settings.
|
||||
LOG_DEBUG("Width: %d, Height: %d", settingsMap[displayWidth], settingsMap[displayHeight]);
|
||||
cfg.pin_cs = settingsMap[displayCS]; // Pin number where CS is connected (-1 = disable)
|
||||
cfg.pin_rst = settingsMap[displayReset];
|
||||
if (settingsMap[displayRotate]) {
|
||||
cfg.panel_width = settingsMap[displayHeight]; // actual displayable width
|
||||
cfg.panel_height = settingsMap[displayWidth]; // actual displayable height
|
||||
LOG_DEBUG("Width: %d, Height: %d", portduino_config.displayWidth, portduino_config.displayHeight);
|
||||
cfg.pin_cs = portduino_config.displayCS.pin; // Pin number where CS is connected (-1 = disable)
|
||||
cfg.pin_rst = portduino_config.displayReset.pin;
|
||||
if (portduino_config.displayRotate) {
|
||||
cfg.panel_width = portduino_config.displayHeight; // actual displayable width
|
||||
cfg.panel_height = portduino_config.displayWidth; // actual displayable height
|
||||
} else {
|
||||
cfg.panel_width = settingsMap[displayWidth]; // actual displayable width
|
||||
cfg.panel_height = settingsMap[displayHeight]; // actual displayable height
|
||||
cfg.panel_width = portduino_config.displayWidth; // actual displayable width
|
||||
cfg.panel_height = portduino_config.displayHeight; // actual displayable height
|
||||
}
|
||||
cfg.offset_x = settingsMap[displayOffsetX]; // Panel offset amount in X direction
|
||||
cfg.offset_y = settingsMap[displayOffsetY]; // Panel offset amount in Y direction
|
||||
cfg.offset_rotation = settingsMap[displayOffsetRotate]; // Rotation direction value offset 0~7 (4~7 is mirrored)
|
||||
cfg.invert = settingsMap[displayInvert]; // Set to true if the light/darkness of the panel is reversed
|
||||
cfg.offset_x = portduino_config.displayOffsetX; // Panel offset amount in X direction
|
||||
cfg.offset_y = portduino_config.displayOffsetY; // Panel offset amount in Y direction
|
||||
cfg.offset_rotation = portduino_config.displayOffsetRotate; // Rotation direction value offset 0~7 (4~7 is mirrored)
|
||||
cfg.invert = portduino_config.displayInvert; // Set to true if the light/darkness of the panel is reversed
|
||||
|
||||
_panel_instance->config(cfg);
|
||||
|
||||
// Configure settings for touch control.
|
||||
if (settingsMap[touchscreenModule]) {
|
||||
if (settingsMap[touchscreenModule] == xpt2046) {
|
||||
if (portduino_config.touchscreenModule) {
|
||||
if (portduino_config.touchscreenModule == xpt2046) {
|
||||
_touch_instance = new lgfx::Touch_XPT2046;
|
||||
} else if (settingsMap[touchscreenModule] == stmpe610) {
|
||||
} else if (portduino_config.touchscreenModule == stmpe610) {
|
||||
_touch_instance = new lgfx::Touch_STMPE610;
|
||||
} else if (settingsMap[touchscreenModule] == ft5x06) {
|
||||
} else if (portduino_config.touchscreenModule == ft5x06) {
|
||||
_touch_instance = new lgfx::Touch_FT5x06;
|
||||
}
|
||||
auto touch_cfg = _touch_instance->config();
|
||||
|
||||
touch_cfg.pin_cs = settingsMap[touchscreenCS];
|
||||
touch_cfg.pin_cs = portduino_config.touchscreenCS.pin;
|
||||
touch_cfg.x_min = 0;
|
||||
touch_cfg.x_max = settingsMap[displayHeight] - 1;
|
||||
touch_cfg.x_max = portduino_config.displayHeight - 1;
|
||||
touch_cfg.y_min = 0;
|
||||
touch_cfg.y_max = settingsMap[displayWidth] - 1;
|
||||
touch_cfg.pin_int = settingsMap[touchscreenIRQ];
|
||||
touch_cfg.y_max = portduino_config.displayWidth - 1;
|
||||
touch_cfg.pin_int = portduino_config.touchscreenIRQ.pin;
|
||||
touch_cfg.bus_shared = true;
|
||||
touch_cfg.offset_rotation = settingsMap[touchscreenRotate];
|
||||
if (settingsMap[touchscreenI2CAddr] != -1) {
|
||||
touch_cfg.i2c_addr = settingsMap[touchscreenI2CAddr];
|
||||
touch_cfg.offset_rotation = portduino_config.touchscreenRotate;
|
||||
if (portduino_config.touchscreenI2CAddr != -1) {
|
||||
touch_cfg.i2c_addr = portduino_config.touchscreenI2CAddr;
|
||||
} else {
|
||||
touch_cfg.spi_host = settingsMap[touchscreenspidev];
|
||||
touch_cfg.spi_host = portduino_config.touchscreen_spi_dev_int;
|
||||
}
|
||||
|
||||
_touch_instance->config(touch_cfg);
|
||||
_panel_instance->setTouch(_touch_instance);
|
||||
}
|
||||
|
||||
#if defined(LGFX_SDL)
|
||||
if (portduino_config.displayPanel == x11) {
|
||||
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance;
|
||||
sdl_panel_->setup();
|
||||
sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER);
|
||||
}
|
||||
#endif
|
||||
setPanel(_panel_instance); // Sets the panel to use.
|
||||
}
|
||||
};
|
||||
@@ -849,9 +949,29 @@ static LGFX *tft = nullptr;
|
||||
#include <lgfx/v1/platforms/esp32s3/Bus_RGB.hpp>
|
||||
#include <lgfx/v1/platforms/esp32s3/Panel_RGB.hpp>
|
||||
|
||||
class PanelInit_ST7701 : public lgfx::Panel_ST7701
|
||||
{
|
||||
public:
|
||||
const uint8_t *getInitCommands(uint8_t listno) const override
|
||||
{
|
||||
// 180 degree hw rotation: vertical flip, horizontal flip
|
||||
static constexpr const uint8_t list1[] = {0x36, 1, 0x10, // MADCTL for vertical flip
|
||||
0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x10, // Command2 BK0 SEL
|
||||
0xC7, 1, 0x04, // SDIR: X-direction Control (Horizontal Flip)
|
||||
0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x00, // Command2 BK0 DIS
|
||||
0xFF, 0xFF};
|
||||
switch (listno) {
|
||||
case 1:
|
||||
return list1;
|
||||
default:
|
||||
return lgfx::Panel_ST7701::getInitCommands(listno);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class LGFX : public lgfx::LGFX_Device
|
||||
{
|
||||
lgfx::Panel_ST7701 _panel_instance;
|
||||
PanelInit_ST7701 _panel_instance;
|
||||
lgfx::Bus_RGB _bus_instance;
|
||||
lgfx::Light_PWM _light_instance;
|
||||
lgfx::Touch_FT5x06 _touch_instance;
|
||||
@@ -962,8 +1082,9 @@ static LGFX *tft = nullptr;
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
|
||||
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || (ARCH_PORTDUINO && HAS_SCREEN != 0)
|
||||
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ST7796_CS) || defined(ILI9341_DRIVER) || \
|
||||
defined(ILI9342_DRIVER) || defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || \
|
||||
(ARCH_PORTDUINO && HAS_SCREEN != 0)
|
||||
#include "SPILock.h"
|
||||
#include "TFTDisplay.h"
|
||||
#include <SPI.h>
|
||||
@@ -994,10 +1115,10 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g
|
||||
backlightEnable = p;
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
if (settingsMap[displayRotate]) {
|
||||
setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]);
|
||||
if (portduino_config.displayRotate) {
|
||||
setGeometry(GEOMETRY_RAWMODE, portduino_config.displayWidth, portduino_config.displayWidth);
|
||||
} else {
|
||||
setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]);
|
||||
setGeometry(GEOMETRY_RAWMODE, portduino_config.displayHeight, portduino_config.displayHeight);
|
||||
}
|
||||
|
||||
#elif defined(SCREEN_ROTATE)
|
||||
@@ -1007,37 +1128,154 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g
|
||||
#endif
|
||||
}
|
||||
|
||||
TFTDisplay::~TFTDisplay()
|
||||
{
|
||||
// Clean up allocated line pixel buffer to prevent memory leak
|
||||
if (linePixelBuffer != nullptr) {
|
||||
free(linePixelBuffer);
|
||||
linePixelBuffer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the buffer to the display memory
|
||||
void TFTDisplay::display(bool fromBlank)
|
||||
{
|
||||
if (fromBlank)
|
||||
tft->fillScreen(TFT_BLACK);
|
||||
// tft->clear();
|
||||
|
||||
concurrency::LockGuard g(spiLock);
|
||||
|
||||
uint16_t x, y;
|
||||
uint32_t x, y;
|
||||
uint32_t y_byteIndex;
|
||||
uint8_t y_byteMask;
|
||||
uint32_t x_FirstPixelUpdate;
|
||||
uint32_t x_LastPixelUpdate;
|
||||
bool isset, dblbuf_isset;
|
||||
uint16_t colorTftMesh, colorTftBlack;
|
||||
bool somethingChanged = false;
|
||||
|
||||
for (y = 0; y < displayHeight; y++) {
|
||||
for (x = 0; x < displayWidth; x++) {
|
||||
auto isset = buffer[x + (y / 8) * displayWidth] & (1 << (y & 7));
|
||||
// Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step
|
||||
colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8);
|
||||
colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8);
|
||||
|
||||
y = 0;
|
||||
while (y < displayHeight) {
|
||||
y_byteIndex = (y / 8) * displayWidth;
|
||||
y_byteMask = (1 << (y & 7));
|
||||
|
||||
// Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas.
|
||||
if (y_byteMask == 1) {
|
||||
if (!fromBlank) {
|
||||
// get src pixel in the page based ordering the OLED lib uses FIXME, super inefficent
|
||||
auto dblbuf_isset = buffer_back[x + (y / 8) * displayWidth] & (1 << (y & 7));
|
||||
if (isset != dblbuf_isset) {
|
||||
tft->drawPixel(x, y, isset ? TFT_MESH : TFT_BLACK);
|
||||
for (x = 0; x < displayWidth; x++) {
|
||||
if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex])
|
||||
break;
|
||||
}
|
||||
} else if (isset) {
|
||||
tft->drawPixel(x, y, TFT_MESH);
|
||||
} else {
|
||||
for (x = 0; x < displayWidth; x++) {
|
||||
if (buffer[x + y_byteIndex] != 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (x >= displayWidth) {
|
||||
// No changed pixels found in these 8 rows, fast-forward to the next 8
|
||||
y = y + 8;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating
|
||||
for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) {
|
||||
isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask;
|
||||
|
||||
if (!fromBlank) {
|
||||
// get src pixel in the page based ordering the OLED lib uses
|
||||
dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask;
|
||||
if (isset != dblbuf_isset) {
|
||||
break;
|
||||
}
|
||||
} else if (isset) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Did we find a pixel that needs updating on this row?
|
||||
if (x_FirstPixelUpdate < displayWidth) {
|
||||
|
||||
// Quickly write out the first changed pixel (saves another array lookup)
|
||||
linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack;
|
||||
x_LastPixelUpdate = x_FirstPixelUpdate;
|
||||
|
||||
// Step 3: copy all remaining pixels in this row into the pixel line buffer,
|
||||
// while also recording the last pixel in the row that needs updating
|
||||
for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) {
|
||||
isset = buffer[x + y_byteIndex] & y_byteMask;
|
||||
linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack;
|
||||
|
||||
if (!fromBlank) {
|
||||
dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask;
|
||||
if (isset != dblbuf_isset) {
|
||||
x_LastPixelUpdate = x;
|
||||
}
|
||||
} else if (isset) {
|
||||
x_LastPixelUpdate = x;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Send the changed pixels on this line to the screen as a single block transfer.
|
||||
// This function accepts pixel data MSB first so it can dump the memory straight out the SPI port.
|
||||
tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1,
|
||||
&linePixelBuffer[x_FirstPixelUpdate]);
|
||||
|
||||
somethingChanged = true;
|
||||
}
|
||||
y++;
|
||||
}
|
||||
// Copy the Buffer to the Back Buffer
|
||||
for (y = 0; y < (displayHeight / 8); y++) {
|
||||
for (x = 0; x < displayWidth; x++) {
|
||||
uint16_t pos = x + y * displayWidth;
|
||||
buffer_back[pos] = buffer[pos];
|
||||
if (somethingChanged)
|
||||
memcpy(buffer_back, buffer, displayBufferSize);
|
||||
}
|
||||
|
||||
void TFTDisplay::sdlLoop()
|
||||
{
|
||||
#if defined(LGFX_SDL)
|
||||
static int lastPressed = 0;
|
||||
static int shuttingDown = false;
|
||||
if (portduino_config.displayPanel == x11) {
|
||||
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance;
|
||||
if (sdl_panel_->loop() && !shuttingDown) {
|
||||
LOG_WARN("Window Closed!");
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
}
|
||||
|
||||
// debounce
|
||||
if (lastPressed != 0 && !lgfx::v1::gpio_in(lastPressed))
|
||||
return;
|
||||
if (!lgfx::v1::gpio_in(37)) {
|
||||
lastPressed = 37;
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_RIGHT, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
} else if (!lgfx::v1::gpio_in(36)) {
|
||||
lastPressed = 36;
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_UP, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
} else if (!lgfx::v1::gpio_in(38)) {
|
||||
lastPressed = 38;
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_DOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
} else if (!lgfx::v1::gpio_in(39)) {
|
||||
lastPressed = 39;
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_LEFT, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
} else if (!lgfx::v1::gpio_in(SDL_SCANCODE_KP_ENTER)) {
|
||||
lastPressed = SDL_SCANCODE_KP_ENTER;
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SELECT, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
} else {
|
||||
lastPressed = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Send a command to the display (low level function)
|
||||
@@ -1050,8 +1288,8 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
backlightEnable->set(true);
|
||||
#if ARCH_PORTDUINO
|
||||
display(true);
|
||||
if (settingsMap[displayBacklight] > 0)
|
||||
digitalWrite(settingsMap[displayBacklight], TFT_BACKLIGHT_ON);
|
||||
if (portduino_config.displayBacklight.pin > 0)
|
||||
digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON);
|
||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
|
||||
tft->wakeup();
|
||||
tft->powerSaveOff();
|
||||
@@ -1074,8 +1312,8 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
backlightEnable->set(false);
|
||||
#if ARCH_PORTDUINO
|
||||
tft->clear();
|
||||
if (settingsMap[displayBacklight] > 0)
|
||||
digitalWrite(settingsMap[displayBacklight], !TFT_BACKLIGHT_ON);
|
||||
if (portduino_config.displayBacklight.pin > 0)
|
||||
digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON);
|
||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
|
||||
tft->sleep();
|
||||
tft->powerSaveOn();
|
||||
@@ -1184,15 +1422,23 @@ bool TFTDisplay::connect()
|
||||
attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING);
|
||||
#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2)
|
||||
tft->setRotation(1); // T-Deck has the TFT in landscape
|
||||
#elif defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR)
|
||||
#elif defined(T_WATCH_S3)
|
||||
tft->setRotation(2); // T-Watch S3 left-handed orientation
|
||||
#elif ARCH_PORTDUINO
|
||||
#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) || defined(T_LORA_PAGER)
|
||||
tft->setRotation(0); // use config.yaml to set rotation
|
||||
#else
|
||||
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
|
||||
#endif
|
||||
tft->fillScreen(TFT_BLACK);
|
||||
|
||||
if (this->linePixelBuffer == NULL) {
|
||||
this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth);
|
||||
|
||||
if (!this->linePixelBuffer) {
|
||||
LOG_ERROR("Not enough memory to create TFT line buffer\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,13 @@ class TFTDisplay : public OLEDDisplay
|
||||
*/
|
||||
TFTDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C);
|
||||
|
||||
// Destructor to clean up allocated memory
|
||||
~TFTDisplay();
|
||||
|
||||
// Write the buffer to the display memory
|
||||
virtual void display() override { display(false); };
|
||||
virtual void display(bool fromBlank);
|
||||
void sdlLoop();
|
||||
|
||||
// Turn the display upside down
|
||||
virtual void flipScreenVertically();
|
||||
@@ -57,4 +61,6 @@ class TFTDisplay : public OLEDDisplay
|
||||
|
||||
// Connect to the display
|
||||
virtual bool connect() override;
|
||||
|
||||
uint16_t *linePixelBuffer = nullptr;
|
||||
};
|
||||
738
src/graphics/VirtualKeyboard.cpp
Normal file
738
src/graphics/VirtualKeyboard.cpp
Normal file
@@ -0,0 +1,738 @@
|
||||
#include "VirtualKeyboard.h"
|
||||
#include "configuration.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "main.h"
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis())
|
||||
{
|
||||
initializeKeyboard();
|
||||
// Set cursor to H(2, 5)
|
||||
cursorRow = 2;
|
||||
cursorCol = 5;
|
||||
}
|
||||
|
||||
VirtualKeyboard::~VirtualKeyboard() {}
|
||||
|
||||
void VirtualKeyboard::initializeKeyboard()
|
||||
{
|
||||
// New 4 row, 11 column keyboard layout:
|
||||
static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'},
|
||||
{'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'},
|
||||
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '},
|
||||
{'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}};
|
||||
|
||||
// Derive layout dimensions and assert they match the configured keyboard grid
|
||||
constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0]));
|
||||
constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0]));
|
||||
static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS");
|
||||
static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS");
|
||||
|
||||
// Initialize all keys to empty first
|
||||
for (int row = 0; row < LAYOUT_ROWS; row++) {
|
||||
for (int col = 0; col < LAYOUT_COLS; col++) {
|
||||
keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0};
|
||||
}
|
||||
}
|
||||
|
||||
// Fill keyboard from the 2D layout
|
||||
for (int row = 0; row < LAYOUT_ROWS; row++) {
|
||||
for (int col = 0; col < LAYOUT_COLS; col++) {
|
||||
char ch = LAYOUT[row][col];
|
||||
// No empty slots in the simplified layout
|
||||
|
||||
VirtualKeyType type = VK_CHAR;
|
||||
if (ch == '\b') {
|
||||
type = VK_BACKSPACE;
|
||||
} else if (ch == '\n') {
|
||||
type = VK_ENTER;
|
||||
} else if (ch == '\x1b') { // ESC
|
||||
type = VK_ESC;
|
||||
} else if (ch == ' ') {
|
||||
type = VK_SPACE;
|
||||
}
|
||||
|
||||
// Make action keys wider to fit text while keeping the last column aligned
|
||||
uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH;
|
||||
keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY)
|
||||
{
|
||||
// Repeat ticking is driven by NotificationRenderer once per frame
|
||||
// Base styles
|
||||
display->setColor(WHITE);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
// Screen geometry
|
||||
const int screenW = display->getWidth();
|
||||
const int screenH = display->getHeight();
|
||||
|
||||
// Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels
|
||||
// Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide
|
||||
const bool isWide = screenW >= 200;
|
||||
|
||||
// Determine last-column label max width
|
||||
display->setFont(FONT_SMALL);
|
||||
const int wENTER = display->getStringWidth("ENTER");
|
||||
int lastColLabelW = wENTER; // ENTER is usually the widest
|
||||
// Smaller padding on very small screens to avoid excessive whitespace
|
||||
const int lastColPad = (screenW <= 128 ? 2 : 6);
|
||||
const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys
|
||||
|
||||
// Always reserve width for the rightmost text column to avoid overlap on small screens
|
||||
int cellW = 0;
|
||||
int leftoverW = 0;
|
||||
{
|
||||
const int leftCols = KEYBOARD_COLS - 1; // 10 input characters
|
||||
int usableW = screenW - reservedLastColW;
|
||||
if (usableW < leftCols) {
|
||||
// Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely)
|
||||
usableW = leftCols;
|
||||
}
|
||||
cellW = usableW / leftCols;
|
||||
leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right)
|
||||
}
|
||||
|
||||
// Dynamic key geometry
|
||||
int cellH = KEY_HEIGHT;
|
||||
int keyboardStartY = 0;
|
||||
if (screenH <= 64) {
|
||||
const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2);
|
||||
const int gapBelowHeader = 0;
|
||||
const int singleLineBoxHeight = FONT_HEIGHT_SMALL;
|
||||
const int gapAboveKeyboard = 0;
|
||||
keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard;
|
||||
if (keyboardStartY < 0)
|
||||
keyboardStartY = 0;
|
||||
if (keyboardStartY > screenH)
|
||||
keyboardStartY = screenH;
|
||||
int keyboardHeight = screenH - keyboardStartY;
|
||||
cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS);
|
||||
} else if (isWide) {
|
||||
// For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width.
|
||||
cellH = std::max((int)KEY_HEIGHT, cellW);
|
||||
|
||||
// Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed.
|
||||
// Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1
|
||||
display->setFont(FONT_SMALL);
|
||||
const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1);
|
||||
const int headerToBoxGap = 1;
|
||||
const int gapAboveKb = 1;
|
||||
const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom
|
||||
int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb);
|
||||
int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS;
|
||||
if (maxCellHAllowed < (int)KEY_HEIGHT)
|
||||
maxCellHAllowed = KEY_HEIGHT;
|
||||
if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) {
|
||||
cellH = maxCellHAllowed;
|
||||
}
|
||||
// Keyboard placement from bottom for wide screens
|
||||
int keyboardHeight = KEYBOARD_ROWS * cellH;
|
||||
keyboardStartY = screenH - keyboardHeight;
|
||||
if (keyboardStartY < 0)
|
||||
keyboardStartY = 0;
|
||||
} else {
|
||||
// Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom
|
||||
cellH = KEY_HEIGHT;
|
||||
int keyboardHeight = KEYBOARD_ROWS * cellH;
|
||||
keyboardStartY = screenH - keyboardHeight;
|
||||
if (keyboardStartY < 0)
|
||||
keyboardStartY = 0;
|
||||
}
|
||||
|
||||
// Draw input area above keyboard
|
||||
drawInputArea(display, offsetX, offsetY, keyboardStartY);
|
||||
|
||||
// Precompute per-column x and width with leftover distributed over left columns for even spacing
|
||||
int colX[KEYBOARD_COLS];
|
||||
int colW[KEYBOARD_COLS];
|
||||
int runningX = offsetX;
|
||||
for (int col = 0; col < KEYBOARD_COLS - 1; ++col) {
|
||||
int wcol = cellW + (col < leftoverW ? 1 : 0);
|
||||
colX[col] = runningX;
|
||||
colW[col] = wcol;
|
||||
runningX += wcol;
|
||||
}
|
||||
// Last column
|
||||
colX[KEYBOARD_COLS - 1] = runningX;
|
||||
colW[KEYBOARD_COLS - 1] = reservedLastColW;
|
||||
|
||||
// Draw keyboard grid
|
||||
for (int row = 0; row < KEYBOARD_ROWS; row++) {
|
||||
for (int col = 0; col < KEYBOARD_COLS; col++) {
|
||||
const VirtualKey &k = keyboard[row][col];
|
||||
if (k.character != 0 || k.type != VK_CHAR) {
|
||||
const bool isLastCol = (col == KEYBOARD_COLS - 1);
|
||||
int x = colX[col];
|
||||
int w = colW[col];
|
||||
int y = offsetY + keyboardStartY + row * cellH;
|
||||
int h = cellH;
|
||||
bool selected = (row == cursorRow && col == cursorCol);
|
||||
drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY)
|
||||
{
|
||||
display->setColor(WHITE);
|
||||
|
||||
const int screenWidth = display->getWidth();
|
||||
const int screenHeight = display->getHeight();
|
||||
// Use the standard small font metrics for input box sizing (restore original size)
|
||||
const int inputLineH = FONT_HEIGHT_SMALL;
|
||||
|
||||
// Header uses the standard small (which may be larger on big screens)
|
||||
display->setFont(FONT_SMALL);
|
||||
int headerHeight = 0;
|
||||
if (!headerText.empty()) {
|
||||
// Draw header and reserve exact font height (plus a tighter gap) to maximize input area
|
||||
display->drawString(offsetX + 2, offsetY, headerText.c_str());
|
||||
if (screenHeight <= 64) {
|
||||
headerHeight = FONT_HEIGHT_SMALL - 2; // 11px
|
||||
} else {
|
||||
headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in
|
||||
}
|
||||
}
|
||||
|
||||
const int boxX = offsetX;
|
||||
const int boxWidth = screenWidth;
|
||||
int boxY;
|
||||
int boxHeight;
|
||||
if (screenHeight <= 64) {
|
||||
const int gapBelowHeader = 0;
|
||||
const int fixedBoxHeight = inputLineH;
|
||||
const int gapAboveKeyboard = 0;
|
||||
boxY = offsetY + headerHeight + gapBelowHeader;
|
||||
boxHeight = fixedBoxHeight;
|
||||
if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) {
|
||||
int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY;
|
||||
boxHeight = std::max(1, fixedBoxHeight - over);
|
||||
}
|
||||
} else {
|
||||
const int gapBelowHeader = 1;
|
||||
int gapAboveKeyboard = 1;
|
||||
int tmpBoxY = offsetY + headerHeight + gapBelowHeader;
|
||||
const int minBoxHeight = inputLineH + 2;
|
||||
int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard;
|
||||
if (availableH < minBoxHeight)
|
||||
availableH = minBoxHeight;
|
||||
boxY = tmpBoxY;
|
||||
boxHeight = availableH;
|
||||
}
|
||||
|
||||
// Draw box border
|
||||
display->drawRect(boxX, boxY, boxWidth, boxHeight);
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
// Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis
|
||||
const int textX = boxX + 2;
|
||||
const int maxTextWidth = boxWidth - 4;
|
||||
const int maxLines = (boxHeight - 2) / inputLineH;
|
||||
if (maxLines >= 2) {
|
||||
// Inner bounds for caret clamping
|
||||
const int innerLeft = boxX + 1;
|
||||
const int innerRight = boxX + boxWidth - 2;
|
||||
const int innerTop = boxY + 1;
|
||||
const int innerBottom = boxY + boxHeight - 2;
|
||||
|
||||
// Wrap text greedily into lines that fit maxTextWidth
|
||||
std::vector<std::string> lines;
|
||||
{
|
||||
std::string remaining = inputText;
|
||||
while (!remaining.empty()) {
|
||||
int bestLen = 0;
|
||||
for (int len = 1; len <= (int)remaining.size(); ++len) {
|
||||
int w = display->getStringWidth(remaining.substr(0, len).c_str());
|
||||
if (w <= maxTextWidth)
|
||||
bestLen = len;
|
||||
else
|
||||
break;
|
||||
}
|
||||
if (bestLen == 0) {
|
||||
// At least show one character to make progress
|
||||
bestLen = 1;
|
||||
}
|
||||
lines.emplace_back(remaining.substr(0, bestLen));
|
||||
remaining.erase(0, bestLen);
|
||||
}
|
||||
}
|
||||
|
||||
const bool scrolledUp = ((int)lines.size() > maxLines);
|
||||
int caretX = textX;
|
||||
int caretY = innerTop;
|
||||
|
||||
// Leave a small top gap to render '...' without replacing the first line
|
||||
const int topInset = 2;
|
||||
const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height
|
||||
int lineY = innerTop + topInset;
|
||||
|
||||
if (scrolledUp) {
|
||||
// Draw three small dots centered horizontally, vertically at the midpoint of the gap
|
||||
// between the inner top and the first line's top baseline. This avoids using a tall glyph.
|
||||
const int firstLineTop = lineY; // baseline top for the first visible line
|
||||
const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested
|
||||
const int centerX = boxX + boxWidth / 2;
|
||||
const int dotSpacing = 3; // px between dots
|
||||
const int dotSize = 1; // small square dot
|
||||
display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize);
|
||||
display->fillRect(centerX, gapMidY, dotSize, dotSize);
|
||||
display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize);
|
||||
}
|
||||
|
||||
// How many lines fit with our top inset and tighter step
|
||||
const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep);
|
||||
const int linesToShow = std::min((int)lines.size(), linesCapacity);
|
||||
const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0;
|
||||
|
||||
for (int i = 0; i < linesToShow; ++i) {
|
||||
const std::string &chunk = lines[startIndex + i];
|
||||
display->drawString(textX, lineY, chunk.c_str());
|
||||
caretX = textX + display->getStringWidth(chunk.c_str());
|
||||
caretY = lineY;
|
||||
lineY += lineStep;
|
||||
}
|
||||
|
||||
// Draw caret at end of the last visible line
|
||||
int caretPadY = 2;
|
||||
if (boxHeight >= inputLineH + 4)
|
||||
caretPadY = 3;
|
||||
int cursorTop = caretY + caretPadY;
|
||||
// Use lineStep so caret height matches the row spacing
|
||||
int cursorH = lineStep - caretPadY * 2;
|
||||
if (cursorH < 1)
|
||||
cursorH = 1;
|
||||
// Clamp vertical bounds to stay inside the inner rect
|
||||
if (cursorTop < innerTop)
|
||||
cursorTop = innerTop;
|
||||
if (cursorTop + cursorH - 1 > innerBottom)
|
||||
cursorH = innerBottom - cursorTop + 1;
|
||||
if (cursorH < 1)
|
||||
cursorH = 1;
|
||||
// Only draw if cursor is inside inner bounds
|
||||
if (caretX >= innerLeft && caretX <= innerRight) {
|
||||
display->drawVerticalLine(caretX, cursorTop, cursorH);
|
||||
}
|
||||
} else {
|
||||
std::string displayText = inputText;
|
||||
int textW = display->getStringWidth(displayText.c_str());
|
||||
std::string scrolled = displayText;
|
||||
if (textW > maxTextWidth) {
|
||||
// Trim from the left until it fits
|
||||
while (textW > maxTextWidth && !scrolled.empty()) {
|
||||
scrolled.erase(0, 1);
|
||||
textW = display->getStringWidth(scrolled.c_str());
|
||||
}
|
||||
// Add leading ellipsis and ensure it still fits
|
||||
if (scrolled != displayText) {
|
||||
scrolled = "..." + scrolled;
|
||||
textW = display->getStringWidth(scrolled.c_str());
|
||||
// If adding ellipsis causes overflow, trim more after the ellipsis
|
||||
while (textW > maxTextWidth && scrolled.size() > 3) {
|
||||
scrolled.erase(3, 1); // remove chars after the ellipsis
|
||||
textW = display->getStringWidth(scrolled.c_str());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Keep textW in sync with what we draw
|
||||
textW = display->getStringWidth(scrolled.c_str());
|
||||
}
|
||||
|
||||
int textY;
|
||||
if (screenHeight <= 64) {
|
||||
textY = boxY + (boxHeight - inputLineH) / 2;
|
||||
} else {
|
||||
const int innerLeft = boxX + 1;
|
||||
const int innerRight = boxX + boxWidth - 2;
|
||||
const int innerTop = boxY + 1;
|
||||
const int innerBottom = boxY + boxHeight - 2;
|
||||
|
||||
// Center text vertically within inner box for single-line, then clamp so it never overlaps borders
|
||||
int innerH = innerBottom - innerTop + 1;
|
||||
textY = innerTop + std::max(0, (innerH - inputLineH) / 2);
|
||||
// Clamp fully inside the inner rect
|
||||
if (textY < innerTop)
|
||||
textY = innerTop;
|
||||
int maxTop = innerBottom - inputLineH + 1;
|
||||
if (textY > maxTop)
|
||||
textY = maxTop;
|
||||
}
|
||||
|
||||
if (!scrolled.empty()) {
|
||||
display->drawString(textX, textY, scrolled.c_str());
|
||||
}
|
||||
|
||||
int cursorX = textX + textW;
|
||||
if (screenHeight > 64) {
|
||||
const int innerRight = boxX + boxWidth - 2;
|
||||
if (cursorX > innerRight)
|
||||
cursorX = innerRight;
|
||||
}
|
||||
|
||||
int cursorTop, cursorH;
|
||||
if (screenHeight <= 64) {
|
||||
cursorH = 10;
|
||||
cursorTop = boxY + (boxHeight - cursorH) / 2;
|
||||
} else {
|
||||
const int innerLeft = boxX + 1;
|
||||
const int innerRight = boxX + boxWidth - 2;
|
||||
const int innerTop = boxY + 1;
|
||||
const int innerBottom = boxY + boxHeight - 2;
|
||||
|
||||
cursorTop = boxY + 2;
|
||||
cursorH = boxHeight - 4;
|
||||
if (cursorH < 1)
|
||||
cursorH = 1;
|
||||
if (cursorTop < innerTop)
|
||||
cursorTop = innerTop;
|
||||
if (cursorTop + cursorH - 1 > innerBottom)
|
||||
cursorH = innerBottom - cursorTop + 1;
|
||||
if (cursorH < 1)
|
||||
cursorH = 1;
|
||||
|
||||
if (cursorX < innerLeft || cursorX > innerRight)
|
||||
return;
|
||||
}
|
||||
|
||||
display->drawVerticalLine(cursorX, cursorTop, cursorH);
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width,
|
||||
uint8_t height, bool isLastCol)
|
||||
{
|
||||
// Draw key content
|
||||
display->setFont(FONT_SMALL);
|
||||
const int fontH = FONT_HEIGHT_SMALL;
|
||||
// Build label and metrics first
|
||||
std::string keyText;
|
||||
if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) {
|
||||
// Keep literal text labels for the action keys on the rightmost column
|
||||
keyText = (key.type == VK_BACKSPACE) ? "BACK"
|
||||
: (key.type == VK_ENTER) ? "ENTER"
|
||||
: (key.type == VK_SPACE) ? "SPACE"
|
||||
: (key.type == VK_ESC) ? "ESC"
|
||||
: "";
|
||||
} else {
|
||||
char c = getCharForKey(key, false);
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
c = c - 'a' + 'A';
|
||||
}
|
||||
keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c);
|
||||
}
|
||||
|
||||
int textWidth = display->getStringWidth(keyText.c_str());
|
||||
// Label alignment
|
||||
// - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly.
|
||||
// - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths.
|
||||
int textX;
|
||||
if (isLastCol) {
|
||||
const int rightPad = 1;
|
||||
textX = x + width - textWidth - rightPad;
|
||||
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;
|
||||
}
|
||||
}
|
||||
int contentTop = y;
|
||||
int contentH = height;
|
||||
if (selected) {
|
||||
display->setColor(WHITE);
|
||||
bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC);
|
||||
|
||||
if (display->getHeight() <= 64 && !isAction) {
|
||||
display->fillRect(x, y, width, height);
|
||||
} else if (isAction) {
|
||||
const int padX = 1;
|
||||
const int padY = 2;
|
||||
int hlW = textWidth + padX * 2;
|
||||
int hlX = textX - padX;
|
||||
|
||||
if (hlX < x) {
|
||||
hlW -= (x - hlX);
|
||||
hlX = x;
|
||||
}
|
||||
int maxW = (x + width) - hlX;
|
||||
if (hlW > maxW)
|
||||
hlW = maxW;
|
||||
if (hlW < 1)
|
||||
hlW = 1;
|
||||
|
||||
int hlH = std::min(fontH + padY * 2, (int)height);
|
||||
int hlY = y + (height - hlH) / 2;
|
||||
display->fillRect(hlX, hlY, hlW, hlH);
|
||||
contentTop = hlY;
|
||||
contentH = hlH;
|
||||
} else {
|
||||
display->fillRect(x, y, width, height);
|
||||
}
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
|
||||
int centeredTextY;
|
||||
if (display->getHeight() <= 64) {
|
||||
centeredTextY = y + (height - fontH) / 2;
|
||||
} else {
|
||||
centeredTextY = contentTop + (contentH - fontH) / 2;
|
||||
}
|
||||
if (display->getHeight() > 64) {
|
||||
if (centeredTextY < contentTop)
|
||||
centeredTextY = contentTop;
|
||||
if (centeredTextY + fontH > contentTop + contentH)
|
||||
centeredTextY = std::max(contentTop, contentTop + contentH - fontH);
|
||||
}
|
||||
|
||||
if (display->getHeight() <= 64 && keyText.size() == 1) {
|
||||
char ch = keyText[0];
|
||||
if (ch == '.' || ch == ',' || ch == ';') {
|
||||
centeredTextY -= 1;
|
||||
}
|
||||
}
|
||||
display->drawString(textX, centeredTextY, keyText.c_str());
|
||||
}
|
||||
|
||||
char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)
|
||||
{
|
||||
if (key.type != VK_CHAR) {
|
||||
return key.character;
|
||||
}
|
||||
|
||||
char c = key.character;
|
||||
|
||||
// Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings
|
||||
if (isLongPress && c >= 'a' && c <= 'z') {
|
||||
c = (char)(c - 'a' + 'A');
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::moveCursorDelta(int dRow, int dCol)
|
||||
{
|
||||
resetTimeout();
|
||||
// wrap around rows and cols in the 4x11 grid
|
||||
int r = (int)cursorRow + dRow;
|
||||
int c = (int)cursorCol + dCol;
|
||||
if (r < 0)
|
||||
r = KEYBOARD_ROWS - 1;
|
||||
else if (r >= KEYBOARD_ROWS)
|
||||
r = 0;
|
||||
if (c < 0)
|
||||
c = KEYBOARD_COLS - 1;
|
||||
else if (c >= KEYBOARD_COLS)
|
||||
c = 0;
|
||||
cursorRow = (uint8_t)r;
|
||||
cursorCol = (uint8_t)c;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::moveCursorUp()
|
||||
{
|
||||
moveCursorDelta(-1, 0);
|
||||
}
|
||||
void VirtualKeyboard::moveCursorDown()
|
||||
{
|
||||
moveCursorDelta(1, 0);
|
||||
}
|
||||
void VirtualKeyboard::moveCursorLeft()
|
||||
{
|
||||
resetTimeout();
|
||||
|
||||
if (cursorCol > 0) {
|
||||
cursorCol--;
|
||||
} else {
|
||||
if (cursorRow > 0) {
|
||||
cursorRow--;
|
||||
cursorCol = KEYBOARD_COLS - 1;
|
||||
} else {
|
||||
cursorRow = KEYBOARD_ROWS - 1;
|
||||
cursorCol = KEYBOARD_COLS - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
void VirtualKeyboard::moveCursorRight()
|
||||
{
|
||||
resetTimeout();
|
||||
|
||||
if (cursorCol < KEYBOARD_COLS - 1) {
|
||||
cursorCol++;
|
||||
} else {
|
||||
if (cursorRow < KEYBOARD_ROWS - 1) {
|
||||
cursorRow++;
|
||||
cursorCol = 0;
|
||||
} else {
|
||||
cursorRow = 0;
|
||||
cursorCol = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::handlePress()
|
||||
{
|
||||
resetTimeout(); // Reset timeout on any input activity
|
||||
|
||||
const VirtualKey &key = keyboard[cursorRow][cursorCol];
|
||||
|
||||
// Don't handle press if the key is empty (but allow special keys)
|
||||
if (key.character == 0 && key.type == VK_CHAR) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For character keys, insert lowercase character
|
||||
if (key.type == VK_CHAR) {
|
||||
insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle non-character keys immediately
|
||||
switch (key.type) {
|
||||
case VK_BACKSPACE:
|
||||
deleteCharacter();
|
||||
break;
|
||||
case VK_ENTER:
|
||||
submitText();
|
||||
break;
|
||||
case VK_SPACE:
|
||||
insertCharacter(' ');
|
||||
break;
|
||||
case VK_ESC:
|
||||
if (onTextEntered) {
|
||||
std::function<void(const std::string &)> callback = onTextEntered;
|
||||
onTextEntered = nullptr;
|
||||
inputText = "";
|
||||
callback("");
|
||||
}
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::handleLongPress()
|
||||
{
|
||||
resetTimeout(); // Reset timeout on any input activity
|
||||
|
||||
const VirtualKey &key = keyboard[cursorRow][cursorCol];
|
||||
|
||||
// Don't handle press if the key is empty (but allow special keys)
|
||||
if (key.character == 0 && key.type == VK_CHAR) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For character keys, insert uppercase/alternate character
|
||||
if (key.type == VK_CHAR) {
|
||||
insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char
|
||||
return;
|
||||
}
|
||||
|
||||
switch (key.type) {
|
||||
case VK_BACKSPACE:
|
||||
// One-shot: delete up to 5 characters on long press
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
if (inputText.empty())
|
||||
break;
|
||||
deleteCharacter();
|
||||
}
|
||||
break;
|
||||
case VK_ENTER:
|
||||
submitText();
|
||||
break;
|
||||
case VK_SPACE:
|
||||
insertCharacter(' ');
|
||||
break;
|
||||
case VK_ESC:
|
||||
if (onTextEntered) {
|
||||
onTextEntered("");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::insertCharacter(char c)
|
||||
{
|
||||
if (inputText.length() < 160) { // Reasonable text length limit
|
||||
inputText += c;
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::deleteCharacter()
|
||||
{
|
||||
if (!inputText.empty()) {
|
||||
inputText.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::submitText()
|
||||
{
|
||||
LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str());
|
||||
|
||||
// Only submit if text is not empty
|
||||
if (!inputText.empty() && onTextEntered) {
|
||||
// Store callback and text to submit before clearing callback
|
||||
std::function<void(const std::string &)> callback = onTextEntered;
|
||||
std::string textToSubmit = inputText;
|
||||
onTextEntered = nullptr;
|
||||
// Don't clear inputText here - let the calling module handle cleanup
|
||||
// inputText = ""; // Removed: keep text visible until module cleans up
|
||||
callback(textToSubmit);
|
||||
} else if (inputText.empty()) {
|
||||
// For empty text, just ignore the submission - don't clear callback
|
||||
// This keeps the virtual keyboard responsive for further input
|
||||
LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active");
|
||||
} else {
|
||||
// No callback available
|
||||
if (screen) {
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualKeyboard::setInputText(const std::string &text)
|
||||
{
|
||||
inputText = text;
|
||||
}
|
||||
|
||||
std::string VirtualKeyboard::getInputText() const
|
||||
{
|
||||
return inputText;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::setHeader(const std::string &header)
|
||||
{
|
||||
headerText = header;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::setCallback(std::function<void(const std::string &)> callback)
|
||||
{
|
||||
onTextEntered = callback;
|
||||
}
|
||||
|
||||
void VirtualKeyboard::resetTimeout()
|
||||
{
|
||||
lastActivityTime = millis();
|
||||
}
|
||||
|
||||
bool VirtualKeyboard::isTimedOut() const
|
||||
{
|
||||
return (millis() - lastActivityTime) > TIMEOUT_MS;
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
80
src/graphics/VirtualKeyboard.h
Normal file
80
src/graphics/VirtualKeyboard.h
Normal file
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE };
|
||||
|
||||
struct VirtualKey {
|
||||
char character;
|
||||
VirtualKeyType type;
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
uint8_t width;
|
||||
uint8_t height;
|
||||
};
|
||||
|
||||
class VirtualKeyboard
|
||||
{
|
||||
public:
|
||||
VirtualKeyboard();
|
||||
~VirtualKeyboard();
|
||||
|
||||
void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY);
|
||||
void setInputText(const std::string &text);
|
||||
std::string getInputText() const;
|
||||
void setHeader(const std::string &header);
|
||||
void setCallback(std::function<void(const std::string &)> callback);
|
||||
|
||||
// Navigation methods for encoder input
|
||||
void moveCursorUp();
|
||||
void moveCursorDown();
|
||||
void moveCursorLeft();
|
||||
void moveCursorRight();
|
||||
void handlePress();
|
||||
void handleLongPress();
|
||||
|
||||
// Timeout management
|
||||
void resetTimeout();
|
||||
bool isTimedOut() const;
|
||||
|
||||
private:
|
||||
static const uint8_t KEYBOARD_ROWS = 4;
|
||||
static const uint8_t KEYBOARD_COLS = 11;
|
||||
static const uint8_t KEY_WIDTH = 9;
|
||||
static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays
|
||||
static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom
|
||||
|
||||
VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS];
|
||||
|
||||
std::string inputText;
|
||||
std::string headerText;
|
||||
std::function<void(const std::string &)> onTextEntered;
|
||||
|
||||
uint8_t cursorRow;
|
||||
uint8_t cursorCol;
|
||||
|
||||
// Timeout management for auto-exit
|
||||
uint32_t lastActivityTime;
|
||||
static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout
|
||||
|
||||
void initializeKeyboard();
|
||||
void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h,
|
||||
bool isLastCol);
|
||||
void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY);
|
||||
|
||||
// Unified cursor movement helper
|
||||
void moveCursorDelta(int dRow, int dCol);
|
||||
|
||||
char getCharForKey(const VirtualKey &key, bool isLongPress = false);
|
||||
void insertCharacter(char c);
|
||||
void deleteCharacter();
|
||||
void submitText();
|
||||
};
|
||||
|
||||
} // namespace graphics
|
||||
@@ -2,7 +2,6 @@
|
||||
#if HAS_SCREEN
|
||||
#include "ClockRenderer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "gps/RTC.h"
|
||||
@@ -190,7 +189,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
// === Set Title, Blank for Clock
|
||||
const char *titleStr = "";
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true);
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
|
||||
#ifdef T_WATCH_S3
|
||||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||
@@ -294,6 +293,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2,
|
||||
isPM ? "pm" : "am");
|
||||
}
|
||||
|
||||
#ifndef USE_EINK
|
||||
xOffset = (isHighResolution) ? 18 : 10;
|
||||
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset,
|
||||
@@ -313,7 +313,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
// === Set Title, Blank for Clock
|
||||
const char *titleStr = "";
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true);
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
|
||||
#ifdef T_WATCH_S3
|
||||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||
|
||||
@@ -94,7 +94,8 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
|
||||
if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat,
|
||||
(storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \
|
||||
ARCH_PORTDUINO) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12,
|
||||
8, imgQuestionL1);
|
||||
@@ -106,7 +107,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
|
||||
#endif
|
||||
} else {
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16,
|
||||
8, imgSFL1);
|
||||
@@ -121,7 +122,8 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
|
||||
} else {
|
||||
// TODO: Raspberry Pi supports more than just the one screen size
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \
|
||||
ARCH_PORTDUINO) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8,
|
||||
imgInfoL1);
|
||||
@@ -261,12 +263,6 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
display->drawString(x + 1, y, "USB");
|
||||
}
|
||||
|
||||
// auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true);
|
||||
|
||||
// display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode);
|
||||
// if (config.display.heading_bold)
|
||||
// display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode);
|
||||
|
||||
uint32_t currentMillis = millis();
|
||||
uint32_t seconds = currentMillis / 1000;
|
||||
uint32_t minutes = seconds / 60;
|
||||
@@ -395,26 +391,35 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
int nameX = (SCREEN_WIDTH - textWidth);
|
||||
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
|
||||
|
||||
// === Second Row: Radio Preset ===
|
||||
auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false);
|
||||
// === Second Row: Role ===
|
||||
auto role = DisplayFormatters::getDeviceRole(config.device.role);
|
||||
char device_role[25];
|
||||
snprintf(device_role, sizeof(device_role), "Role: %s", role);
|
||||
textWidth = display->getStringWidth(device_role);
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
display->drawString(nameX, getTextPositions(display)[line++], device_role);
|
||||
|
||||
// === Third Row: Radio Preset ===
|
||||
auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset);
|
||||
|
||||
char regionradiopreset[25];
|
||||
const char *region = myRegion ? myRegion->name : NULL;
|
||||
if (region != nullptr) {
|
||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
|
||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "Reg: %s/%s", region, mode);
|
||||
}
|
||||
textWidth = display->getStringWidth(regionradiopreset);
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset);
|
||||
|
||||
// === Third Row: Frequency / ChanNum ===
|
||||
// === Fourth Row: Frequency / ChanNum ===
|
||||
char frequencyslot[35];
|
||||
char freqStr[16];
|
||||
float freq = RadioLibInterface::instance->getFreq();
|
||||
snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
|
||||
if (config.lora.channel_num == 0) {
|
||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %smhz", freqStr);
|
||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr);
|
||||
} else {
|
||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %smhz (%d)", freqStr, config.lora.channel_num);
|
||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num);
|
||||
}
|
||||
size_t len = strlen(frequencyslot);
|
||||
if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) {
|
||||
@@ -424,7 +429,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
display->drawString(nameX, getTextPositions(display)[line++], frequencyslot);
|
||||
|
||||
// === Fourth Row: Channel Utilization ===
|
||||
// === Fifth Row: Channel Utilization ===
|
||||
const char *chUtil = "ChUtil:";
|
||||
char chUtilPercentage[10];
|
||||
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
|
||||
@@ -441,7 +446,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2;
|
||||
int starting_position = centerofscreen - total_line_content_width;
|
||||
|
||||
display->drawString(starting_position, getTextPositions(display)[line++], chUtil);
|
||||
display->drawString(starting_position, getTextPositions(display)[line], chUtil);
|
||||
|
||||
// Force 56% or higher to show a full 100% bar, text would still show related percent.
|
||||
if (chutil_percent >= 61) {
|
||||
@@ -478,14 +483,14 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height);
|
||||
}
|
||||
|
||||
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[4],
|
||||
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++],
|
||||
chUtilPercentage);
|
||||
}
|
||||
|
||||
// ****************************
|
||||
// * System Screen *
|
||||
// ****************************
|
||||
void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->clear();
|
||||
display->setFont(FONT_SMALL);
|
||||
@@ -629,6 +634,33 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
display->drawString(nameX, getTextPositions(display)[line], uptimeStr);
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************
|
||||
// * Chirpy Screen *
|
||||
// ****************************
|
||||
void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->clear();
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
int line = 1;
|
||||
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
|
||||
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
|
||||
int textX_offset = 10;
|
||||
if (isHighResolution) {
|
||||
iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3);
|
||||
iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2;
|
||||
textX_offset = textX_offset * 4;
|
||||
display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez);
|
||||
} else {
|
||||
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
|
||||
}
|
||||
|
||||
int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2);
|
||||
display->drawString(textX, getTextPositions(display)[line++], "Hello");
|
||||
textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2);
|
||||
display->drawString(textX, getTextPositions(display)[line++], "World!");
|
||||
}
|
||||
} // namespace DebugRenderer
|
||||
} // namespace graphics
|
||||
#endif
|
||||
@@ -31,8 +31,11 @@ void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state
|
||||
// LoRa information display
|
||||
void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// Memory screen display
|
||||
void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
// System screen display
|
||||
void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// Chirpy screen display
|
||||
void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
} // namespace DebugRenderer
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
@@ -10,11 +10,17 @@
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#include "main.h"
|
||||
#include "mesh/MeshTypes.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/CannedMessageModule.h"
|
||||
#include "modules/KeyVerificationModule.h"
|
||||
|
||||
#include "modules/TraceRouteModule.h"
|
||||
#include <functional>
|
||||
|
||||
extern uint16_t TFT_MESH;
|
||||
|
||||
namespace graphics
|
||||
@@ -23,6 +29,47 @@ menuHandler::screenMenus menuHandler::menuQueue = menu_none;
|
||||
bool test_enabled = false;
|
||||
uint8_t test_count = 0;
|
||||
|
||||
void menuHandler::loraMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Region Picker", "Device Role"};
|
||||
enum optionsNumbers { Back = 0, lora_picker = 1, device_role_picker = 2 };
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "LoRa Actions";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 3;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
// No action
|
||||
} else if (selected == lora_picker) {
|
||||
menuHandler::menuQueue = menuHandler::lora_picker;
|
||||
} else if (selected == device_role_picker) {
|
||||
menuHandler::menuQueue = menuHandler::device_role_picker;
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::OnboardMessage()
|
||||
{
|
||||
static const char *optionsArray[] = {"OK", "Got it!"};
|
||||
enum optionsNumbers { OK, got };
|
||||
BannerOverlayOptions bannerOptions;
|
||||
#if HAS_TFT
|
||||
bannerOptions.message = "Welcome to Meshtastic!\nSwipe to navigate and\nlong press to select\nor open a menu.";
|
||||
#elif defined(BUTTON_PIN)
|
||||
bannerOptions.message = "Welcome to Meshtastic!\nClick to navigate and\nlong press to select\nor open a menu.";
|
||||
#else
|
||||
bannerOptions.message = "Welcome to Meshtastic!\nUse the Select button\nto open menus\nand make selections.";
|
||||
#endif
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 2;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
menuHandler::menuQueue = menuHandler::no_timeout_lora_picker;
|
||||
screen->runNow();
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
{
|
||||
static const char *optionsArray[] = {"Back",
|
||||
@@ -51,12 +98,14 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
"PH_915",
|
||||
"ANZ_433",
|
||||
"KZ_433",
|
||||
"KZ_863"};
|
||||
"KZ_863",
|
||||
"NP_865",
|
||||
"BR_902"};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Set the LoRa region";
|
||||
bannerOptions.durationMs = duration;
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 25;
|
||||
bannerOptions.optionsCount = 27;
|
||||
bannerOptions.InitialSelected = 0;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) {
|
||||
@@ -93,6 +142,40 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::DeviceRolePicker()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
|
||||
enum optionsNumbers {
|
||||
Back = 0,
|
||||
devicerole_client = 1,
|
||||
devicerole_clientmute = 2,
|
||||
devicerole_lostandfound = 3,
|
||||
devicerole_tracker = 4
|
||||
};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Device Role";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 5;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::lora_Menu;
|
||||
screen->runNow();
|
||||
return;
|
||||
} else if (selected == devicerole_client) {
|
||||
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||
} else if (selected == devicerole_clientmute) {
|
||||
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE;
|
||||
} else if (selected == devicerole_lostandfound) {
|
||||
config.device.role = meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND;
|
||||
} else if (selected == devicerole_tracker) {
|
||||
config.device.role = meshtastic_Config_DeviceConfig_Role_TRACKER;
|
||||
}
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::TwelveHourPicker()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
|
||||
@@ -115,6 +198,22 @@ void menuHandler::TwelveHourPicker()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
// Reusable confirmation prompt function
|
||||
void menuHandler::showConfirmationBanner(const char *message, std::function<void()> onConfirm)
|
||||
{
|
||||
static const char *confirmOptions[] = {"No", "Yes"};
|
||||
BannerOverlayOptions confirmBanner;
|
||||
confirmBanner.message = message;
|
||||
confirmBanner.optionsArrayPtr = confirmOptions;
|
||||
confirmBanner.optionsCount = 2;
|
||||
confirmBanner.bannerCallback = [onConfirm](int confirmSelected) -> void {
|
||||
if (confirmSelected == 1) {
|
||||
onConfirm();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(confirmBanner);
|
||||
}
|
||||
|
||||
void menuHandler::ClockFacePicker()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Digital", "Analog"};
|
||||
@@ -151,6 +250,7 @@ void menuHandler::TZPicker()
|
||||
"US/Mountain",
|
||||
"US/Central",
|
||||
"US/Eastern",
|
||||
"BR/Brasilia",
|
||||
"UTC",
|
||||
"EU/Western",
|
||||
"EU/"
|
||||
@@ -165,7 +265,7 @@ void menuHandler::TZPicker()
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Pick Timezone";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 17;
|
||||
bannerOptions.optionsCount = 19;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == 0) {
|
||||
menuHandler::menuQueue = menuHandler::clock_menu;
|
||||
@@ -184,25 +284,27 @@ void menuHandler::TZPicker()
|
||||
strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
||||
} else if (selected == 7) { // Eastern
|
||||
strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
||||
} else if (selected == 8) { // UTC
|
||||
strncpy(config.device.tzdef, "UTC", sizeof(config.device.tzdef));
|
||||
} else if (selected == 9) { // EU/Western
|
||||
} else if (selected == 8) { // Brazil
|
||||
strncpy(config.device.tzdef, "BRT3", sizeof(config.device.tzdef));
|
||||
} else if (selected == 9) { // UTC
|
||||
strncpy(config.device.tzdef, "UTC0", sizeof(config.device.tzdef));
|
||||
} else if (selected == 10) { // EU/Western
|
||||
strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef));
|
||||
} else if (selected == 10) { // EU/Central
|
||||
} else if (selected == 11) { // EU/Central
|
||||
strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef));
|
||||
} else if (selected == 11) { // EU/Eastern
|
||||
} else if (selected == 12) { // EU/Eastern
|
||||
strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef));
|
||||
} else if (selected == 12) { // Asia/Kolkata
|
||||
} else if (selected == 13) { // Asia/Kolkata
|
||||
strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef));
|
||||
} else if (selected == 13) { // China
|
||||
} else if (selected == 14) { // China
|
||||
strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef));
|
||||
} else if (selected == 14) { // AU/AWST
|
||||
} else if (selected == 15) { // AU/AWST
|
||||
strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef));
|
||||
} else if (selected == 15) { // AU/ACST
|
||||
} else if (selected == 16) { // AU/ACST
|
||||
strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
|
||||
} else if (selected == 16) { // AU/AEST
|
||||
} else if (selected == 17) { // AU/AEST
|
||||
strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
|
||||
} else if (selected == 17) { // NZ
|
||||
} else if (selected == 18) { // NZ
|
||||
strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef));
|
||||
}
|
||||
if (selected != 0) {
|
||||
@@ -260,7 +362,7 @@ void menuHandler::messageResponseMenu()
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Dismiss) {
|
||||
screen->dismissCurrentFrame();
|
||||
screen->hideCurrentFrame();
|
||||
} else if (selected == Preset) {
|
||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||
@@ -288,21 +390,24 @@ void menuHandler::messageResponseMenu()
|
||||
|
||||
void menuHandler::homeBaseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Backlight, Position, Preset, Freetext, Bluetooth, Sleep, enumEnd };
|
||||
enum optionsNumbers { Back, Backlight, Position, Preset, Freetext, Sleep, enumEnd };
|
||||
|
||||
static const char *optionsArray[enumEnd] = {"Back"};
|
||||
static int optionsEnumArray[enumEnd] = {Back};
|
||||
int options = 1;
|
||||
|
||||
#ifdef PIN_EINK_EN
|
||||
#if defined(PIN_EINK_EN) || defined(PCA_PIN_EINK_EN)
|
||||
optionsArray[options] = "Toggle Backlight";
|
||||
optionsEnumArray[options++] = Backlight;
|
||||
#else
|
||||
optionsArray[options] = "Sleep Screen";
|
||||
optionsEnumArray[options++] = Sleep;
|
||||
#endif
|
||||
|
||||
optionsArray[options] = "Send Position";
|
||||
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||
optionsArray[options] = "Send Position";
|
||||
} else {
|
||||
optionsArray[options] = "Send Node Info";
|
||||
}
|
||||
optionsEnumArray[options++] = Position;
|
||||
optionsArray[options] = "New Preset Msg";
|
||||
optionsEnumArray[options++] = Preset;
|
||||
@@ -310,8 +415,6 @@ void menuHandler::homeBaseMenu()
|
||||
optionsArray[options] = "New Freetext Msg";
|
||||
optionsEnumArray[options++] = Freetext;
|
||||
}
|
||||
optionsArray[options] = "Bluetooth Toggle";
|
||||
optionsEnumArray[options++] = Bluetooth;
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Home Action";
|
||||
@@ -320,12 +423,24 @@ void menuHandler::homeBaseMenu()
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Backlight) {
|
||||
#ifdef PIN_EINK_EN
|
||||
if (digitalRead(PIN_EINK_EN) == HIGH) {
|
||||
#if defined(PIN_EINK_EN)
|
||||
if (uiconfig.screen_brightness == 1) {
|
||||
uiconfig.screen_brightness = 0;
|
||||
digitalWrite(PIN_EINK_EN, LOW);
|
||||
} else {
|
||||
uiconfig.screen_brightness = 1;
|
||||
digitalWrite(PIN_EINK_EN, HIGH);
|
||||
}
|
||||
saveUIConfig();
|
||||
#elif defined(PCA_PIN_EINK_EN)
|
||||
if (uiconfig.screen_brightness == 1) {
|
||||
uiconfig.screen_brightness = 0;
|
||||
io.digitalWrite(PCA_PIN_EINK_EN, LOW);
|
||||
} else {
|
||||
uiconfig.screen_brightness = 1;
|
||||
io.digitalWrite(PCA_PIN_EINK_EN, HIGH);
|
||||
}
|
||||
saveUIConfig();
|
||||
#endif
|
||||
} else if (selected == Sleep) {
|
||||
screen->setOn(false);
|
||||
@@ -336,9 +451,6 @@ void menuHandler::homeBaseMenu()
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||
} else if (selected == Freetext) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST);
|
||||
} else if (selected == Bluetooth) {
|
||||
menuQueue = bluetooth_toggle_menu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -375,19 +487,25 @@ void menuHandler::textMessageBaseMenu()
|
||||
|
||||
void menuHandler::systemBaseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Notifications, ScreenOptions, PowerMenu, Test, enumEnd };
|
||||
enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, FrameToggles, Test, enumEnd };
|
||||
static const char *optionsArray[enumEnd] = {"Back"};
|
||||
static int optionsEnumArray[enumEnd] = {Back};
|
||||
int options = 1;
|
||||
|
||||
optionsArray[options] = "Notifications";
|
||||
optionsEnumArray[options++] = Notifications;
|
||||
#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || \
|
||||
defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT
|
||||
#if defined(ST7789_CS) || defined(ST7796_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || \
|
||||
defined(USE_SH1107) || defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT
|
||||
optionsArray[options] = "Screen Options";
|
||||
optionsEnumArray[options++] = ScreenOptions;
|
||||
#endif
|
||||
|
||||
optionsArray[options] = "Frame Visiblity Toggle";
|
||||
optionsEnumArray[options++] = FrameToggles;
|
||||
|
||||
optionsArray[options] = "Bluetooth Toggle";
|
||||
optionsEnumArray[options++] = Bluetooth;
|
||||
|
||||
optionsArray[options] = "Reboot/Shutdown";
|
||||
optionsEnumArray[options++] = PowerMenu;
|
||||
|
||||
@@ -411,9 +529,15 @@ void menuHandler::systemBaseMenu()
|
||||
} else if (selected == PowerMenu) {
|
||||
menuHandler::menuQueue = menuHandler::power_menu;
|
||||
screen->runNow();
|
||||
} else if (selected == FrameToggles) {
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
} else if (selected == Test) {
|
||||
menuHandler::menuQueue = menuHandler::test_menu;
|
||||
screen->runNow();
|
||||
} else if (selected == Bluetooth) {
|
||||
menuQueue = bluetooth_toggle_menu;
|
||||
screen->runNow();
|
||||
} else if (selected == Back && !test_enabled) {
|
||||
test_count++;
|
||||
if (test_count > 4) {
|
||||
@@ -426,7 +550,7 @@ void menuHandler::systemBaseMenu()
|
||||
|
||||
void menuHandler::favoriteBaseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Preset, Freetext, Remove, enumEnd };
|
||||
enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd };
|
||||
static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"};
|
||||
static int optionsEnumArray[enumEnd] = {Back, Preset};
|
||||
int options = 2;
|
||||
@@ -435,6 +559,8 @@ void menuHandler::favoriteBaseMenu()
|
||||
optionsArray[options] = "New Freetext Msg";
|
||||
optionsEnumArray[options++] = Freetext;
|
||||
}
|
||||
optionsArray[options] = "Trace Route";
|
||||
optionsEnumArray[options++] = TraceRoute;
|
||||
optionsArray[options] = "Remove Favorite";
|
||||
optionsEnumArray[options++] = Remove;
|
||||
|
||||
@@ -451,6 +577,10 @@ void menuHandler::favoriteBaseMenu()
|
||||
} else if (selected == Remove) {
|
||||
menuHandler::menuQueue = menuHandler::remove_favorite;
|
||||
screen->runNow();
|
||||
} else if (selected == TraceRoute) {
|
||||
if (traceRouteModule) {
|
||||
traceRouteModule->launch(graphics::UIRenderer::currentFavoriteNodeNum);
|
||||
}
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -468,6 +598,7 @@ void menuHandler::positionBaseMenu()
|
||||
optionsArray[options] = "Compass Calibrate";
|
||||
optionsEnumArray[options++] = CompassCalibrate;
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Position Action";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -489,12 +620,12 @@ void menuHandler::positionBaseMenu()
|
||||
|
||||
void menuHandler::nodeListMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Favorite, Verify, Reset };
|
||||
static const char *optionsArray[] = {"Back", "Add Favorite", "Key Verification", "Reset NodeDB"};
|
||||
enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd };
|
||||
static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Node Action";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 4;
|
||||
bannerOptions.optionsCount = 5;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Favorite) {
|
||||
menuQueue = add_favorite;
|
||||
@@ -505,6 +636,9 @@ void menuHandler::nodeListMenu()
|
||||
} else if (selected == Reset) {
|
||||
menuQueue = reset_node_db_menu;
|
||||
screen->runNow();
|
||||
} else if (selected == TraceRoute) {
|
||||
menuQueue = trace_route_menu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -658,7 +792,7 @@ void menuHandler::BrightnessPickerMenu()
|
||||
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190)
|
||||
// For HELTEC devices, use analogWrite to control backlight
|
||||
analogWrite(VTFT_LEDA, uiconfig.screen_brightness);
|
||||
#elif defined(ST7789_CS)
|
||||
#elif defined(ST7789_CS) || defined(ST7796_CS)
|
||||
static_cast<TFTDisplay *>(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness);
|
||||
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
|
||||
screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness);
|
||||
@@ -701,7 +835,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 10;
|
||||
bannerOptions.bannerCallback = [display](int selected) -> void {
|
||||
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || HAS_TFT
|
||||
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT
|
||||
uint8_t TFT_MESH_r = 0;
|
||||
uint8_t TFT_MESH_g = 0;
|
||||
uint8_t TFT_MESH_b = 0;
|
||||
@@ -857,19 +991,46 @@ void menuHandler::removeFavoriteMenu()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::traceRouteMenu()
|
||||
{
|
||||
screen->showNodePicker("Node to Trace", 30000, [](uint32_t nodenum) -> void {
|
||||
LOG_INFO("Menu: Node picker selected node 0x%08x, traceRouteModule=%p", nodenum, traceRouteModule);
|
||||
if (traceRouteModule) {
|
||||
traceRouteModule->startTraceRoute(nodenum);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void menuHandler::testMenu()
|
||||
{
|
||||
|
||||
static const char *optionsArray[] = {"Back", "Number Picker"};
|
||||
enum optionsNumbers { Back, NumberPicker, ShowChirpy };
|
||||
static const char *optionsArray[4] = {"Back"};
|
||||
static int optionsEnumArray[4] = {Back};
|
||||
int options = 1;
|
||||
|
||||
optionsArray[options] = "Number Picker";
|
||||
optionsEnumArray[options++] = NumberPicker;
|
||||
|
||||
optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy";
|
||||
optionsEnumArray[options++] = ShowChirpy;
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
std::string message = "Test to Run?\n";
|
||||
bannerOptions.message = message.c_str();
|
||||
bannerOptions.message = "Hidden Test Menu";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 2;
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == 1) {
|
||||
if (selected == NumberPicker) {
|
||||
menuQueue = number_test;
|
||||
screen->runNow();
|
||||
} else if (selected == ShowChirpy) {
|
||||
screen->toggleFrameVisibility("chirpy");
|
||||
screen->setFrames(Screen::FOCUS_SYSTEM);
|
||||
|
||||
} else {
|
||||
menuQueue = system_base_menu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -968,7 +1129,7 @@ void menuHandler::screenOptionsMenu()
|
||||
}
|
||||
|
||||
// Only show screen color for TFT displays
|
||||
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || HAS_TFT
|
||||
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT
|
||||
optionsArray[options] = "Screen Color";
|
||||
optionsEnumArray[options++] = ScreenColor;
|
||||
#endif
|
||||
@@ -1066,6 +1227,116 @@ void menuHandler::keyVerificationFinalPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
void menuHandler::FrameToggles_menu()
|
||||
{
|
||||
enum optionsNumbers {
|
||||
Finish,
|
||||
nodelist,
|
||||
nodelist_lastheard,
|
||||
nodelist_hopsignal,
|
||||
nodelist_distance,
|
||||
nodelist_bearings,
|
||||
gps,
|
||||
lora,
|
||||
clock,
|
||||
show_favorites,
|
||||
enumEnd
|
||||
};
|
||||
static const char *optionsArray[enumEnd] = {"Finish"};
|
||||
static int optionsEnumArray[enumEnd] = {Finish};
|
||||
int options = 1;
|
||||
|
||||
// Track last selected index (not enum value!)
|
||||
static int lastSelectedIndex = 0;
|
||||
|
||||
#ifndef USE_EINK
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List";
|
||||
optionsEnumArray[options++] = nodelist;
|
||||
#endif
|
||||
#ifdef USE_EINK
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard";
|
||||
optionsEnumArray[options++] = nodelist_lastheard;
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal";
|
||||
optionsEnumArray[options++] = nodelist_hopsignal;
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance";
|
||||
optionsEnumArray[options++] = nodelist_distance;
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings";
|
||||
optionsEnumArray[options++] = nodelist_bearings;
|
||||
|
||||
optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
|
||||
optionsEnumArray[options++] = gps;
|
||||
#endif
|
||||
|
||||
optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa";
|
||||
optionsEnumArray[options++] = lora;
|
||||
|
||||
optionsArray[options] = screen->isFrameHidden("clock") ? "Show Clock" : "Hide Clock";
|
||||
optionsEnumArray[options++] = clock;
|
||||
|
||||
optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites";
|
||||
optionsEnumArray[options++] = show_favorites;
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Show/Hide Frames";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value
|
||||
|
||||
bannerOptions.bannerCallback = [options](int selected) mutable -> void {
|
||||
// Find the index of selected in optionsEnumArray
|
||||
int idx = 0;
|
||||
for (; idx < options; ++idx) {
|
||||
if (optionsEnumArray[idx] == selected)
|
||||
break;
|
||||
}
|
||||
lastSelectedIndex = idx;
|
||||
|
||||
if (selected == Finish) {
|
||||
screen->setFrames(Screen::FOCUS_DEFAULT);
|
||||
} else if (selected == nodelist) {
|
||||
screen->toggleFrameVisibility("nodelist");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
} else if (selected == nodelist_lastheard) {
|
||||
screen->toggleFrameVisibility("nodelist_lastheard");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
} else if (selected == nodelist_hopsignal) {
|
||||
screen->toggleFrameVisibility("nodelist_hopsignal");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
} else if (selected == nodelist_distance) {
|
||||
screen->toggleFrameVisibility("nodelist_distance");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
} else if (selected == nodelist_bearings) {
|
||||
screen->toggleFrameVisibility("nodelist_bearings");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
} else if (selected == gps) {
|
||||
screen->toggleFrameVisibility("gps");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
} else if (selected == lora) {
|
||||
screen->toggleFrameVisibility("lora");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
} else if (selected == clock) {
|
||||
screen->toggleFrameVisibility("clock");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
} else if (selected == show_favorites) {
|
||||
screen->toggleFrameVisibility("show_favorites");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
{
|
||||
if (menuQueue != menu_none)
|
||||
@@ -1073,9 +1344,18 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
switch (menuQueue) {
|
||||
case menu_none:
|
||||
break;
|
||||
case lora_Menu:
|
||||
loraMenu();
|
||||
break;
|
||||
case lora_picker:
|
||||
LoraRegionPicker();
|
||||
break;
|
||||
case device_role_picker:
|
||||
DeviceRolePicker();
|
||||
break;
|
||||
case no_timeout_lora_picker:
|
||||
LoraRegionPicker(0);
|
||||
break;
|
||||
case TZ_picker:
|
||||
TZPicker();
|
||||
break;
|
||||
@@ -1129,6 +1409,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case remove_favorite:
|
||||
removeFavoriteMenu();
|
||||
break;
|
||||
case trace_route_menu:
|
||||
traceRouteMenu();
|
||||
break;
|
||||
case test_menu:
|
||||
testMenu();
|
||||
break;
|
||||
@@ -1156,6 +1439,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case power_menu:
|
||||
powerMenu();
|
||||
break;
|
||||
case FrameToggles:
|
||||
FrameToggles_menu();
|
||||
break;
|
||||
case throttle_message:
|
||||
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
|
||||
break;
|
||||
|
||||
@@ -9,7 +9,10 @@ class menuHandler
|
||||
public:
|
||||
enum screenMenus {
|
||||
menu_none,
|
||||
lora_Menu,
|
||||
lora_picker,
|
||||
device_role_picker,
|
||||
no_timeout_lora_picker,
|
||||
TZ_picker,
|
||||
twelve_hour_picker,
|
||||
clock_face_picker,
|
||||
@@ -36,12 +39,18 @@ class menuHandler
|
||||
system_base_menu,
|
||||
key_verification_init,
|
||||
key_verification_final_prompt,
|
||||
throttle_message
|
||||
trace_route_menu,
|
||||
throttle_message,
|
||||
FrameToggles
|
||||
};
|
||||
static screenMenus menuQueue;
|
||||
|
||||
static void OnboardMessage();
|
||||
static void LoraRegionPicker(uint32_t duration = 30000);
|
||||
static void loraMenu();
|
||||
static void DeviceRolePicker();
|
||||
static void handleMenuSwitch(OLEDDisplay *display);
|
||||
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
|
||||
static void clockMenu();
|
||||
static void TZPicker();
|
||||
static void TwelveHourPicker();
|
||||
@@ -64,6 +73,7 @@ class menuHandler
|
||||
static void shutdownMenu();
|
||||
static void addFavoriteMenu();
|
||||
static void removeFavoriteMenu();
|
||||
static void traceRouteMenu();
|
||||
static void testMenu();
|
||||
static void numberTest();
|
||||
static void wifiBaseMenu();
|
||||
@@ -71,6 +81,7 @@ class menuHandler
|
||||
static void notificationsMenu();
|
||||
static void screenOptionsMenu();
|
||||
static void powerMenu();
|
||||
static void FrameToggles_menu();
|
||||
|
||||
private:
|
||||
static void saveUIConfig();
|
||||
|
||||
@@ -137,7 +137,11 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
|
||||
display->drawString(cursorX + 1, fontY, textChunk.c_str());
|
||||
}
|
||||
display->drawString(cursorX, fontY, textChunk.c_str());
|
||||
#if defined(OLED_UA) || defined(OLED_RU)
|
||||
cursorX += display->getStringWidth(textChunk.c_str(), textChunk.length(), true);
|
||||
#else
|
||||
cursorX += display->getStringWidth(textChunk.c_str());
|
||||
#endif
|
||||
i = nextControl;
|
||||
continue;
|
||||
}
|
||||
@@ -155,7 +159,12 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
|
||||
display->drawString(cursorX + 1, fontY, remaining.c_str());
|
||||
}
|
||||
display->drawString(cursorX, fontY, remaining.c_str());
|
||||
#if defined(OLED_UA) || defined(OLED_RU)
|
||||
cursorX += display->getStringWidth(remaining.c_str(), remaining.length(), true);
|
||||
#else
|
||||
cursorX += display->getStringWidth(remaining.c_str());
|
||||
#endif
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -273,7 +282,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
currentKey ^= ((size_t)mp.id << 24);
|
||||
|
||||
if (cachedKey != currentKey) {
|
||||
LOG_INFO("Message cache key is misssed cachedKey=0x%0x, currentKey=0x%x", cachedKey, currentKey);
|
||||
LOG_INFO("Onscreen message scroll cache key needs updating: cachedKey=0x%0x, currentKey=0x%x", cachedKey, currentKey);
|
||||
|
||||
// Cache miss - regenerate lines and heights
|
||||
cachedLines = generateLines(display, headerStr, messageBuf, textWidth);
|
||||
@@ -374,10 +383,16 @@ std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerS
|
||||
} else {
|
||||
word += ch;
|
||||
std::string test = line + word;
|
||||
// Keep these lines for diagnostics
|
||||
// LOG_INFO("Char: '%c' (0x%02X)", ch, (unsigned char)ch);
|
||||
// LOG_INFO("Current String: %s", test.c_str());
|
||||
if (display->getStringWidth(test.c_str()) > textWidth) {
|
||||
// Keep these lines for diagnostics
|
||||
// LOG_INFO("Char: '%c' (0x%02X)", ch, (unsigned char)ch);
|
||||
// LOG_INFO("Current String: %s", test.c_str());
|
||||
// Note: there are boolean comparison uint16 (getStringWidth) with int (textWidth), hope textWidth is always positive :)
|
||||
#if defined(OLED_UA) || defined(OLED_RU)
|
||||
uint16_t strWidth = display->getStringWidth(test.c_str(), test.length(), true);
|
||||
#else
|
||||
uint16_t strWidth = display->getStringWidth(test.c_str());
|
||||
#endif
|
||||
if (strWidth > textWidth) {
|
||||
if (!line.empty())
|
||||
lines.push_back(line);
|
||||
line = word;
|
||||
|
||||
@@ -7,10 +7,18 @@
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/images.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#if HAS_BUTTON
|
||||
#include "input/ButtonThread.h"
|
||||
#endif
|
||||
#include "main.h"
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#if HAS_TRACKBALL
|
||||
#include "input/TrackballInterruptImpl1.h"
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
#include "esp_task_wdt.h"
|
||||
@@ -18,6 +26,11 @@
|
||||
|
||||
using namespace meshtastic;
|
||||
|
||||
#if HAS_BUTTON
|
||||
// Global button thread pointer defined in main.cpp
|
||||
extern ::ButtonThread *UserButtonThread;
|
||||
#endif
|
||||
|
||||
// External references to global variables from Screen.cpp
|
||||
extern std::vector<std::string> functionSymbol;
|
||||
extern std::string functionSymbolString;
|
||||
@@ -36,8 +49,11 @@ const int *NotificationRenderer::optionsEnumPtr = nullptr;
|
||||
std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL;
|
||||
bool NotificationRenderer::pauseBanner = false;
|
||||
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
||||
|
||||
uint32_t NotificationRenderer::numDigits = 0;
|
||||
uint32_t NotificationRenderer::currentNumber = 0;
|
||||
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
||||
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
||||
|
||||
uint32_t pow_of_10(uint32_t n)
|
||||
{
|
||||
@@ -70,9 +86,13 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
|
||||
void NotificationRenderer::resetBanner()
|
||||
{
|
||||
notificationTypeEnum previousType = current_notification_type;
|
||||
|
||||
alertBannerMessage[0] = '\0';
|
||||
current_notification_type = notificationTypeEnum::none;
|
||||
|
||||
OnScreenKeyboardModule::instance().clearPopup();
|
||||
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||
inEvent.kbchar = 0;
|
||||
curSelected = 0;
|
||||
@@ -85,18 +105,44 @@ void NotificationRenderer::resetBanner()
|
||||
currentNumber = 0;
|
||||
|
||||
nodeDB->pause_sort(false);
|
||||
|
||||
// If we're exiting from text_input (virtual keyboard), stop module and trigger frame update
|
||||
// to ensure any messages received during keyboard use are now displayed
|
||||
if (previousType == notificationTypeEnum::text_input && screen) {
|
||||
OnScreenKeyboardModule::instance().stop(false);
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0')
|
||||
resetBanner();
|
||||
if (!isOverlayBannerShowing() || pauseBanner)
|
||||
// Handle text_input notifications first - they have their own timeout/banner logic
|
||||
if (current_notification_type == notificationTypeEnum::text_input) {
|
||||
// Check for timeout and reset if needed for text input
|
||||
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
drawTextInput(display, state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
|
||||
resetBanner();
|
||||
}
|
||||
|
||||
// Exit if no banner is showing or banner is paused
|
||||
if (!isOverlayBannerShowing() || pauseBanner) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (current_notification_type) {
|
||||
case notificationTypeEnum::none:
|
||||
// Do nothing - no notification to display
|
||||
break;
|
||||
case notificationTypeEnum::text_input:
|
||||
// Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch.
|
||||
break;
|
||||
case notificationTypeEnum::text_banner:
|
||||
case notificationTypeEnum::selection_picker:
|
||||
drawAlertBannerOverlay(display, state);
|
||||
@@ -267,12 +313,9 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) {
|
||||
std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name);
|
||||
strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1);
|
||||
|
||||
} else {
|
||||
snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF));
|
||||
}
|
||||
// make temp buffer for name
|
||||
// fi
|
||||
if (i == curSelected) {
|
||||
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
|
||||
if (isHighResolution) {
|
||||
@@ -286,7 +329,8 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
}
|
||||
scratchLineBuffer[scratchLineNum][39] = '\0';
|
||||
} else {
|
||||
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 36);
|
||||
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39);
|
||||
scratchLineBuffer[scratchLineNum][39] = '\0';
|
||||
}
|
||||
linePointers[linesShown] = scratchLineBuffer[scratchLineNum++];
|
||||
}
|
||||
@@ -383,7 +427,9 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
|
||||
|
||||
uint8_t firstOptionToShow = 0;
|
||||
if (alertBannerOptions > 0) {
|
||||
if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) {
|
||||
if (visibleTotalLines - lineCount == 1) {
|
||||
firstOptionToShow = curSelected;
|
||||
} else if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) {
|
||||
if (curSelected > alertBannerOptions - visibleTotalLines + lineCount)
|
||||
firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount;
|
||||
else
|
||||
@@ -392,6 +438,9 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
|
||||
firstOptionToShow = 0;
|
||||
}
|
||||
}
|
||||
// Useful log line for troubleshooting:
|
||||
/* LOG_WARN("alertBannerOptions: %u, curSelected: %u, visibleTotalLines: %u, lineCount: %u, firstOptionToShow: %u",
|
||||
alertBannerOptions, curSelected, visibleTotalLines, lineCount, firstOptionToShow); */
|
||||
|
||||
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
|
||||
if (i == curSelected) {
|
||||
@@ -570,10 +619,182 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi
|
||||
"Please be patient and do not power off.");
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
// Delegate session to OnScreenKeyboardModule
|
||||
auto &osk = OnScreenKeyboardModule::instance();
|
||||
|
||||
if (!osk.isActive()) {
|
||||
LOG_INFO("Virtual keyboard is not active - resetting banner");
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
if (inEvent.inputEvent != INPUT_BROKER_NONE) {
|
||||
osk.handleInput(inEvent);
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE; // consume
|
||||
}
|
||||
|
||||
// Draw. If draw returns false, session ended (timeout or cancel)
|
||||
if (!osk.draw(display)) {
|
||||
// Session ended, ensure banner reset and restore frames
|
||||
resetBanner();
|
||||
if (screen)
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
}
|
||||
|
||||
bool NotificationRenderer::isOverlayBannerShowing()
|
||||
{
|
||||
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);
|
||||
}
|
||||
|
||||
void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs)
|
||||
{
|
||||
if (!title || !content || current_notification_type != notificationTypeEnum::text_input)
|
||||
return;
|
||||
OnScreenKeyboardModule::instance().showPopup(title, content, durationMs);
|
||||
}
|
||||
|
||||
// drawKeyboardMessagePopup removed; OnScreenKeyboardModule handles popup drawing within draw()
|
||||
|
||||
// Custom inverted color version for keyboard popup - black background with white text
|
||||
void NotificationRenderer::drawInvertedNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[],
|
||||
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth)
|
||||
{
|
||||
bool is_picker = false;
|
||||
uint16_t lineCount = 0;
|
||||
// === Layout Configuration ===
|
||||
constexpr uint16_t hPadding = 5;
|
||||
constexpr uint16_t vPadding = 2;
|
||||
bool needs_bell = false;
|
||||
uint16_t lineWidths[totalLines] = {0};
|
||||
uint16_t lineLengths[totalLines] = {0};
|
||||
|
||||
if (maxWidth != 0)
|
||||
is_picker = true;
|
||||
|
||||
// Setup font and alignment
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
while (lines[lineCount] != nullptr) {
|
||||
auto newlinePointer = strchr(lines[lineCount], '\n');
|
||||
if (newlinePointer)
|
||||
lineLengths[lineCount] = (newlinePointer - lines[lineCount]);
|
||||
else
|
||||
lineLengths[lineCount] = strlen(lines[lineCount]);
|
||||
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true);
|
||||
if (!is_picker) {
|
||||
if (lineWidths[lineCount] > maxWidth)
|
||||
maxWidth = lineWidths[lineCount];
|
||||
}
|
||||
lineCount++;
|
||||
}
|
||||
|
||||
uint16_t boxWidth = hPadding * 2 + maxWidth;
|
||||
if (needs_bell) {
|
||||
if (isHighResolution && boxWidth <= 150)
|
||||
boxWidth += 26;
|
||||
if (!isHighResolution && boxWidth <= 100)
|
||||
boxWidth += 20;
|
||||
}
|
||||
|
||||
uint16_t screenHeight = display->height();
|
||||
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
|
||||
uint16_t boxHeight = contentHeight + vPadding * 2;
|
||||
if (visibleTotalLines == 1) {
|
||||
boxHeight += (isHighResolution) ? 4 : 3;
|
||||
}
|
||||
|
||||
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
|
||||
if (totalLines > visibleTotalLines) {
|
||||
boxWidth += (isHighResolution) ? 4 : 2;
|
||||
}
|
||||
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
|
||||
|
||||
// === Draw Box with INVERTED COLORS ===
|
||||
// Add outer separation pixels (1-pixel white background around the box)
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
|
||||
|
||||
// Make outer corners round by filling back with背景色 (BLACK for separation)
|
||||
display->setColor(BLACK);
|
||||
// Top-left outer corner
|
||||
display->fillRect(boxLeft - 1, boxTop - 1, 1, 1);
|
||||
// Top-right outer corner
|
||||
display->fillRect(boxLeft + boxWidth, boxTop - 1, 1, 1);
|
||||
// Bottom-left outer corner
|
||||
display->fillRect(boxLeft - 1, boxTop + boxHeight, 1, 1);
|
||||
// Bottom-right outer corner
|
||||
display->fillRect(boxLeft + boxWidth, boxTop + boxHeight, 1, 1);
|
||||
|
||||
// Draw single pixel black border
|
||||
display->setColor(BLACK);
|
||||
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
|
||||
|
||||
// Make inner corners round by filling white pixels at corners
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(boxLeft, boxTop, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
|
||||
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||
|
||||
// Fill interior with BLACK (inverted)
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft + 1, boxTop + 1, boxWidth - 2, boxHeight - 2);
|
||||
|
||||
// === Draw Content with WHITE text on BLACK background ===
|
||||
display->setColor(WHITE);
|
||||
int16_t lineY = boxTop + vPadding;
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
|
||||
|
||||
char lineBuffer[lineLengths[i] + 1];
|
||||
strncpy(lineBuffer, lines[i], lineLengths[i]);
|
||||
lineBuffer[lineLengths[i]] = '\0';
|
||||
|
||||
// For keyboard popup, treat first line as title if it's different from others
|
||||
if (i == 0 && lineCount > 1) {
|
||||
// Title line - use inverted colors (white background, black text)
|
||||
display->setColor(WHITE);
|
||||
int background_yOffset = 1;
|
||||
// Check for low hanging characters
|
||||
if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) {
|
||||
background_yOffset = -1;
|
||||
}
|
||||
display->fillRect(boxLeft + 1, boxTop + 1, boxWidth - 2, effectiveLineHeight - background_yOffset);
|
||||
display->setColor(BLACK);
|
||||
int yOffset = 3;
|
||||
display->drawString(textX, lineY - yOffset, lineBuffer);
|
||||
display->setColor(WHITE); // Reset to white for next lines
|
||||
lineY += (effectiveLineHeight - 2 - background_yOffset);
|
||||
} else {
|
||||
// Content lines - white text on black background
|
||||
display->drawString(textX, lineY, lineBuffer);
|
||||
lineY += effectiveLineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// === Scroll Bar (if needed) ===
|
||||
if (totalLines > visibleTotalLines) {
|
||||
const uint8_t scrollBarWidth = 5;
|
||||
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
|
||||
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight;
|
||||
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
|
||||
|
||||
float ratio = (float)visibleTotalLines / totalLines;
|
||||
uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4);
|
||||
float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines);
|
||||
uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
|
||||
|
||||
display->setColor(WHITE);
|
||||
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
|
||||
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
@@ -3,6 +3,10 @@
|
||||
#include "OLEDDisplay.h"
|
||||
#include "OLEDDisplayUi.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/VirtualKeyboard.h"
|
||||
#include "modules/OnScreenKeyboardModule.h"
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#define MAX_LINES 5
|
||||
|
||||
namespace graphics
|
||||
@@ -22,16 +26,22 @@ class NotificationRenderer
|
||||
static std::function<void(int)> alertBannerCallback;
|
||||
static uint32_t numDigits;
|
||||
static uint32_t currentNumber;
|
||||
static VirtualKeyboard *virtualKeyboard;
|
||||
static std::function<void(const std::string &)> textInputCallback;
|
||||
|
||||
static bool pauseBanner;
|
||||
|
||||
static void resetBanner();
|
||||
static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs);
|
||||
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNumberPicker(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],
|
||||
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);
|
||||
static void drawInvertedNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[],
|
||||
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);
|
||||
|
||||
static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
@@ -194,7 +194,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
|
||||
}
|
||||
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
|
||||
if (isHighResolution) {
|
||||
@@ -879,7 +879,26 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
config.display.heading_bold = false;
|
||||
|
||||
const char *displayLine = ""; // Initialize to empty string by default
|
||||
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
|
||||
bool usePhoneGPS = (ourNode && nodeDB->hasValidPosition(ourNode) &&
|
||||
config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED);
|
||||
|
||||
if (usePhoneGPS) {
|
||||
// Phone-provided GPS is active
|
||||
displayLine = "Phone GPS";
|
||||
int yOffset = (isHighResolution) ? 3 : 1;
|
||||
if (isHighResolution) {
|
||||
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
|
||||
imgSatellite_height, imgSatellite, display);
|
||||
} else {
|
||||
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
|
||||
imgSatellite);
|
||||
}
|
||||
int xOffset = (isHighResolution) ? 6 : 0;
|
||||
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
|
||||
} else if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||
// GPS disabled / not present
|
||||
if (config.position.fixed_position) {
|
||||
displayLine = "Fixed GPS";
|
||||
} else {
|
||||
@@ -896,6 +915,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
int xOffset = (isHighResolution) ? 6 : 0;
|
||||
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
|
||||
} else {
|
||||
// Onboard GPS
|
||||
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
|
||||
}
|
||||
|
||||
@@ -922,32 +942,61 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
|
||||
// If GPS is off, no need to display these parts
|
||||
if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) {
|
||||
// === Second Row: Last GPS Fix ===
|
||||
if (gpsStatus->getLastFixMillis() > 0) {
|
||||
uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix
|
||||
uint32_t days = delta / 86400;
|
||||
uint32_t hours = (delta % 86400) / 3600;
|
||||
uint32_t mins = (delta % 3600) / 60;
|
||||
uint32_t secs = delta % 60;
|
||||
|
||||
// === Second Row: Date ===
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
char datetimeStr[25];
|
||||
bool showTime = false; // set to true for full datetime
|
||||
UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime);
|
||||
char fullLine[40];
|
||||
snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr);
|
||||
display->drawString(0, getTextPositions(display)[line++], fullLine);
|
||||
char buf[32];
|
||||
#if defined(USE_EINK)
|
||||
// E-Ink: skip seconds, show only days/hours/mins
|
||||
if (days > 0) {
|
||||
snprintf(buf, sizeof(buf), " Last: %ud %uh", days, hours);
|
||||
} else if (hours > 0) {
|
||||
snprintf(buf, sizeof(buf), " Last: %uh %um", hours, mins);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), " Last: %um", mins);
|
||||
}
|
||||
#else
|
||||
// Non E-Ink: include seconds where useful
|
||||
if (days > 0) {
|
||||
snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours);
|
||||
} else if (hours > 0) {
|
||||
snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins);
|
||||
} else if (mins > 0) {
|
||||
snprintf(buf, sizeof(buf), "Last: %um %us", mins, secs);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "Last: %us", secs);
|
||||
}
|
||||
#endif
|
||||
|
||||
display->drawString(0, getTextPositions(display)[line++], buf);
|
||||
} else {
|
||||
display->drawString(0, getTextPositions(display)[line++], "Last: ?");
|
||||
}
|
||||
|
||||
// === Third Row: Latitude ===
|
||||
char latStr[32];
|
||||
snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7);
|
||||
snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7);
|
||||
display->drawString(x, getTextPositions(display)[line++], latStr);
|
||||
|
||||
// === Fourth Row: Longitude ===
|
||||
char lonStr[32];
|
||||
snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7);
|
||||
snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7);
|
||||
display->drawString(x, getTextPositions(display)[line++], lonStr);
|
||||
|
||||
// === Fifth Row: Altitude ===
|
||||
char DisplayLineTwo[32] = {0};
|
||||
int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode))
|
||||
? ourNode->position.altitude
|
||||
: geoCoord.getAltitude();
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET);
|
||||
snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), "Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET);
|
||||
} else {
|
||||
snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude());
|
||||
snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), "Alt: %.0im", geoCoord.getAltitude());
|
||||
}
|
||||
display->drawString(x, getTextPositions(display)[line++], DisplayLineTwo);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "NodeDB.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include <OLEDDisplay.h>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#ifdef USE_EINK
|
||||
|
||||
#include "EinkDisplayFonts.h"
|
||||
|
||||
// Created by https://oleddisplay.squix.ch/ Consider a donation
|
||||
@@ -1182,3 +1184,5 @@ const uint8_t Monospaced_plain_30[] PROGMEM = {
|
||||
0xF0, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0xE0, 0x00, 0xFE, 0x03, 0x00, 0xE0, 0x80, 0x7F,
|
||||
0x00, 0x00, 0xE0, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10 // 255
|
||||
};
|
||||
|
||||
#endif // USE_EINK
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef EINKDISPLAYFONTS_h
|
||||
#define EINKDISPLAYFONTS_h
|
||||
|
||||
#ifdef USE_EINK
|
||||
|
||||
#ifdef ARDUINO
|
||||
#include <Arduino.h>
|
||||
#elif __MBED__
|
||||
@@ -11,4 +13,7 @@
|
||||
* Monospaced Plain 30
|
||||
*/
|
||||
extern const uint8_t Monospaced_plain_30[] PROGMEM;
|
||||
|
||||
#endif // USE_EINK
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#ifdef OLED_CS
|
||||
|
||||
#include "OLEDDisplayFontsCS.h"
|
||||
|
||||
// Font generated or edited with the glyphEditor
|
||||
@@ -1860,4 +1862,6 @@ const uint8_t ArialMT_Plain_24_CS[] PROGMEM = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0xC0, 0xF0, 0x01, 0x06, 0xC0, 0x80, 0x0F, 0x07, 0x00,
|
||||
0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0xC0, 0xC0, 0x1F, 0x00, 0xC0, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00,
|
||||
0x06, // 255
|
||||
};
|
||||
};
|
||||
|
||||
#endif // OLED_CS
|
||||
@@ -1,4 +1,5 @@
|
||||
// trunk-ignore-all(clang-format): Preserve long lines
|
||||
#ifdef OLED_PL
|
||||
#include "OLEDDisplayFontsPL.h"
|
||||
|
||||
const uint8_t ArialMT_Plain_10_PL[] PROGMEM = {
|
||||
@@ -1310,4 +1311,6 @@ const uint8_t ArialMT_Plain_24_PL[] PROGMEM = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x80, 0x00, 0xFE, 0x03, 0xE0, 0x00, 0xFC, 0x00, 0x60, 0xC0, 0x1F, 0x00, 0x20, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 253
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 254
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0xC0, 0xF0, 0x01, 0x06, 0xC0, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0xC0, 0xC0, 0x1F, 0x00, 0xC0, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 255
|
||||
};
|
||||
};
|
||||
|
||||
#endif // OLED_PL
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user