Compare commits

...

43 Commits

Author SHA1 Message Date
Ben Meadors
81bde47cd4 Move pb string length method and fix linker error 2026-02-08 10:14:40 -06:00
Ben Meadors
370f62a8c9 Refactor test utilities to use portable delay function and include necessary headers 2026-02-08 09:42:05 -06:00
Ben Meadors
a43ce34143 Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-08 08:35:20 -06:00
niccellular
fa5631523e Fix embedded null byte truncation in ATAK strings
Fix bug where UIDs, callsigns, and messages containing embedded null
bytes (0x00) are truncated during compression/decompression. The issue
occurs because strlen() stops at the first null byte, but nanopb char
arrays can contain embedded nulls that are valid data.

This causes Android device UIDs like "ANDROID-e7e455b40002429d" to be
truncated to "ANDROID-e7e455b4" (16 chars instead of 24), breaking
direct messages and contact identification.

The fix adds a pb_string_length() helper that scans for the actual
string length by finding the last non-null character, rather than
stopping at the first null.

Added comprehensive unit tests to prevent regressions:
- Normal strings without embedded nulls
- Strings with embedded null bytes
- Empty strings and edge cases
- Android UID pattern testing
- Multiple embedded nulls scenarios

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-08 08:54:15 -05:00
oscgonfer
7cbab4838c Feat/add sen5x (#7245)
* Move PMSA003I to separate class and update AQ telemetry

* AirQualityTelemetry module not depend on PM sensor presence

* Remove commented line

* Fixes on PMS class

* Add missing warmup period to wakeUp function

* Fixes on compilation for different variants

* Add functions to check for I2C bus speed and set it

* Add ScreenFonts.h

Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de>

* PMSA003I 1st round test

* Fix I2C scan speed

* Fix minor issues and bring back I2C SPEED def

* Remove PMSA003I library as its no longer needed

* Remove unused I2C speed functions and cleanup

* Cleanup of SEN5X specific code added from switching branches
* Remove SCAN_I2C_CLOCK_SPEED block as its not needed
* Remove associated functions for setting I2C speed

* Unify build epoch to add flag in platformio-custom.py (#7917)

* Unify build_epoch replacement logic in platformio-custom

* Missed one

* Fix build error in rak_wismesh_tap_v2 (#7905)

In the logs was:
"No screen resolution defined in build_flags. Please define DISPLAY_SIZE."

set according to similar devices.

* Put guards in place around debug heap operations (#7955)

* Put guards in place around debug heap operations

* Add macros to clean up code

* Add pointer as well

* Cleanup

* Fix memory leak in NextHopRouter: always free packet copy when removing from pending

* Formatting

* Only queue 2 client notification

* Merge pull request #7965 from compumike/compumike/fix-nrf52-bluetooth-memory-leak

Fix memory leak in `NRF52Bluetooth`: allocate `BluetoothStatus` on stack, not heap

* Merge pull request #7964 from compumike/compumike/fix-nimble-bluetooth-memory-leak

Fix memory leak in `NimbleBluetooth`: allocate `BluetoothStatus` on stack, not heap

* Update protobufs (#7973)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* T-Lora Pager: Support LR1121 and SX1280 models (#7956)

* T-Lora Pager: Support LR1121 and SX1280 models

* Remove ifdefs

* Trunk

* Trunk

* Static memory pool allocation (#7966)

* Static memory pool

* Initializer

* T-Lora Pager: Support LR1121 and SX1280 models (#7956)

* T-Lora Pager: Support LR1121 and SX1280 models

* Remove ifdefs

---------

Co-authored-by: WillyJL <me@willyjl.dev>

* Portduino dynamic alloc

* Missed

* Drop the limit

* Update meshtastic-esp8266-oled-ssd1306 digest to 0cbc26b (#7977)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Fix json report crashes on esp32 (#7978)

* Tweak maximums

* Fix DRAM overflow on old esp32 targets

* Guard bad time warning logs using GPS_DEBUG (#7897)

In 2.7.7 / 2.7.8 we introduced some new checks for time accuracy.

In combination, these result in a spamming of the logs when a bad time is found

When the GPS is active, we're calling the GPS thread every 0.2secs.

So this log could be printed 4,500 times in a no-lock scenario :)

Reserve this experience for developers using GPS_DEBUG.

Fixes https://github.com/meshtastic/firmware/issues/7896

* Scale probe buffer size based on current baud rate (#7975)

* Scale probe buffer size based on current baud rate

* Throttle bad time validation logging and fix time comparison logic

* Remove comment

* Missed the other instances

* Copy pasta

* Fix GPS gm_mktime memory leak (#7981)

* Fix overflow of time value (#7984)

* Fix overflow of time value

* Revert "Fix overflow of time value"

This reverts commit 0847969201.

* That got boogered up

* Remove PMSA003 include from modules

* Add flag to exclude air quality module

* Rework PMSA003I to align with new I2C scanner

* Reworks AQ telemetry to match new dynamic allocation method
* Adds VBLE_I2C_CLOCK_SPEED build flag for sensors with different I2C speed requirements
* Reworks PMSA003I

* Move add sensor template to separate file

* Split telemetry on screen options

* Add variable I2C clock compile flag

* Added to Seeed Xiao S3 as demo

* Fix drawFrame in AQ module

* Module settings override to i2cScan module function

* Move to CAN_RECLOCK_I2C per architecture

* Add reclock function in TelemetrySensor.cpp
* Add flag in ESP32 common

* Minor fix

* Move I2C reclock function to src/detect

* Fix uninitMemberVar errors and compile issue

* Make sleep, wakeUp functions generic

* Fix STM32 builds

* Add exclude AQ sensor to builds that have environmental sensor excludes
* Add includes to AddI2CSensorTemplate.h

* SEN5X first pass

* WIP Sen5X functions

* Further (non-working) progress in SEN5X

* WIP Sen5X functions

* Changes on SEN5X library - removing pm_env as well

* Small cleanup of SEN5X sensors

* Minor change for SEN5X detection

* Remove dup code

* Enable PM sensor before sending telemetry.

This enables the PM sensor for a predefined period to allow for warmup.

Once telemetry is sent, the sensor shuts down again.

* Small cleanups in SEN5X sensor

* Add dynamic measurement interval for SEN5X

* Only disable SEN5X if enough time after reading.

* Idle for SEN5X on communication error

* Cleanup of logs and remove unnecessary delays

* Small TODO

* Settle on uint16_t for SEN5X PM data

* Make AQTelemetry sensors non-exclusive

* Implementation of cleaning in FS prefs and cleanup

* Remove unnecessary LOGS
* Add cleaning date storage in FS
* Report non-cumulative PN

* Bring back detection code for SEN5X after branch rebase

* Add placeholder for admin message

* Add VOC measurements and persistence (WIP)

* Adds VOC measurements and state
* Still not working on VOC Index persistence
* Should it stay in continuous mode?

* Add one-shot mode config flag to SEN5X

* Add nan checks on sensor data from SEN5X

* Working implementation on VOCState

* Adds initial timer for SEN55 to not sleep if VOCstate is not stable (1h)
* Adds conditions for stability and sensor state

* Fixes on VOC state and mode swtiching

* Adds a new RHT/Gas only mode, with 3600s stabilization time
* Fixes the VOCState buffer mismatch
* Fixes SEN50/54/55 model mistake

* Adapt SEN5X to new sensor list structure. Improve reclock.

* Improve reClockI2C conditions for different variants
* Add sleep, wakeUp, pendingForReady, hasSleep functions to PM sensors to save battery
* Add SEN5X

* Fix merge errors

* Update library dependencies in platformio.ini

* Fix unitialized variables in SEN5X constructor

* Fix missing import

* Cleanup of SEN5X class

* Exclude AQ sensor from wio-e5 due to flash limitations

* Fix I2C clock change logic

* Fix trunk

* Fix on condition in reclock

* Add check on polling interval of sen5x

---------

Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de>
Co-authored-by: Nashui-Yan <yannashui10@gmail.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
Co-authored-by: WillyJL <me@willyjl.dev>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 07:08:09 -06:00
Ben Meadors
53231ae4b1 Added toggable config and default for larger screens to enable / hide bubbles on chat messages (#9560)
* Added toggable config and default for largeer screens to enable / hide bubbles on chat messages

* Refactor message bubble rendering logic for improved layout and consistency

* Move osk_found initialization for trackball/encoder devices before module setup to fix missing keyboard for L1

* Utilize current checks for consistency

* Reverted last changes

---------

Co-authored-by: Jason P <applewiz@mac.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2026-02-07 15:41:31 -06:00
github-actions[bot]
5280caf9d8 Update protobufs (#9559)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-02-07 06:24:56 -06:00
Colby Dillion
ba016fd91a Fix hop_limit upgrade detection (#9550)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-07 06:19:25 -06:00
renovate[bot]
e2cf401ad3 Update meshtastic/device-ui digest to 6c75195 (#9553)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-07 06:16:30 -06:00
github-actions[bot]
f73d18384d Upgrade trunk (#9547)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-06 06:22:34 -06:00
github-actions[bot]
11bb2ee84e Upgrade trunk (#9368)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-02-05 05:53:48 -06:00
renovate[bot]
a324c4af10 Update meshtastic-esp8266-oled-ssd1306 digest to 21e484f (#9533)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-04 19:30:12 -06:00
renovate[bot]
5df5ab2790 Update Adafruit MPU6050 to v2.2.8 (#9534)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 17:19:11 -06:00
renovate[bot]
74ea6206d9 Update NeoPixel to v1.15.3 (#9530)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 17:13:44 -06:00
renovate[bot]
b238744445 Update Adafruit MPU6050 to v2.2.7 (#9525)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 17:12:01 -06:00
Ben Meadors
be5f0a9ade Implement UDP multicast handler start/stop to ensure proper lifecycle (#9524)
* Implement UDP multicast handler start/stop to ensure proper lifecycle

* Add close method to AsyncUDP and improve UDP multicast handler lifecycle management

* Guard portduino
2026-02-04 11:15:38 -06:00
Jason P
b008c7a170 Fix config.display.use_long_node_name not saving (#9522) 2026-02-03 12:21:51 -06:00
Eric Sesterhenn
c8a9cdc148 Make sure we always return a value in NodeDB::restorePreferences() (#9516)
In case FScom is not defined there is no return statement. This
moves the return outside of the ifdef to make sure a defined
value is returned.
2026-02-03 06:23:49 -06:00
Jonathan Bennett
644fa5b54e Power off control pin on Thinknode m5 during deepsleep and add RTC (#9510)
* Power off control pin on Thinknode m5 during deepsleep

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Finish HAS_RTC cleanup

* Add RTC for Thinknode M5

* Don't double-init Wire

* Specify the RTC chip directly rather than use SensorRtcHelper.
Saves a bit of flash, and avoid mis-detection

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-03 06:07:33 -06:00
Ben Meadors
e9d4485bb5 Merge remote-tracking branch 'origin/develop' 2026-01-30 13:56:11 -06:00
Jonathan Bennett
7b03980e0a Refuse to send legacy DMs simply because the remote public key is unknown (#9485)
* Refuse to send legacy DMs simply because the remote public key is unknown

* Update src/mesh/Router.cpp - use more specific error

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/mesh/Router.cpp - more detail in warning message

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-30 13:46:24 -06:00
Ben Meadors
004179c045 Fix OTA filename determination to use unified format for ESP32 (#9488) 2026-01-30 13:46:11 -06:00
Jonathan Bennett
8f630bfcf3 Fix typo in LED state comment
And removed unused define
2026-01-30 12:52:17 -06:00
scobert969
7bbfe99fbe Add on-screen keyboard to InkHUD (#9445)
* Added keyboard option to menu. Shows a keyboard layout but does not type.

* Keyboard types into text box and wraps.

* send FreeText messages from the send submenu

- renamed `KEYBOARD` action to `FREE_TEXT` and moved its menu location
to the send submenu
- opening the FreeText applet from the menu keeps the menu open and
disabled the timeout
- the FreeText applet writes to inkhud->freetext
- the sending a canned message checks inkhud->freetext and if it isn't
empty, sends and clears the inkhud->freetext

* Text scrolls along with input

* handle free text message completion as an event

implements `handleFreeText` and `OnFreeText()` for system applets to
interface with the FreeText Applet

The FreeText Applet generates an `OnFreeText` event when completing a
message which is handled by the first system applet with the
`handleFreeText` flag set to true.

The Menu Applet now handles this event.

* call `onFreeText` whenever the FreeText Applet exits

allows the menu to consistently restart its auto-close timeout

* Add text cursor

* Change UI to remove the header and make text box longer
Keyboard displays captial letters for legibility
Keyboard types captial letters with long press

* center FreeText keys and draw symbolic buttons

Move input field and keyboard drawing to their own functions:
- `drawInputField()`
- `drawKeyboard()`

Store the keys in a 1-dimensional array

Implement a matching array, `keyWidths`, to set key widths relative to
the font size

* Add character limit and counter

* Fix softlock when hitting character limit

* Move text box as its own menu page

* rework FreeTextApplet into KeyboardApplet

- The Keyboard Applet renders an on-screen keyboard at the lower portion
of the screen.
- Calling `inkhud->openKeyboard()` sends all the user applets to the
background and resizes the first system applet with `handleFreeText` set
to True to fit above the on-screen keyboard
- `inkhud->closeKeyboard()` reverses this layout change

* Fix input box rendering and add character limit to menu free text

* remove FREE_TEXT menu page and use the FREE_TEXT menu action solely

* force update when changing the free text message

* reorganize KeyboardApplet

- add comments after each row of `key[]` and `keyWidths[]` to preserve
formatting

- The selected key is now set using the key index directly

- rowWidths are pre-calculated in the KeyboardApplet constructor

- removed `drawKeyboard()` and implemented `drawKeyLabel()`

* implement `Renderer::clearTile()` to clear the region below a tile

* add parameter to forceUpdate() for re-rendering the full screen

setting the `all` parameter to true in `inkhud->forceUpdate()` now
causes the full screen buffer to clear an re-render. This is helpful for
when sending applets to the background and the UI needs a clean canvas.

System Applets can now set the `alwaysRender` flag true which causes it
to re-render on every screen update. This is set to true in the Battery
Icon Applet.

* clean up tile clearing loops

* implement dirty rendering to let applets draw over their previous render

- `Applet::requestUpdate()` now has an optional flag to keep the old
canvas

- If honored, the renderer calls `render(true)` which runs
`onDirtyRender()` instead of `onRender()` for said applet

- The renderer will not call a dirty render if the full screen is
getting re-rendered

* simplify arithmetic in clearTile for better understanding

* combine Applet::onRender() and Applet::onDirtyRender() into Applet::onRender(bool full)

- add new `full` parameter to onRender() in every applet. This parameter
can be ignored by most applets.

- `Applet::requestUpdate()` has an optional flag that requests a full
render by default

* implement tile and partial rendering in KeyboardApplet

* add comment for drawKeyLabel()

* improve clarity of byte operations in clearTile()

* remove typo and commented code

* fix inaccurate comments

* add null check to openKeyboard() and closeKeyboard()

---------

Co-authored-by: zeropt <ferr0fluidmann@gmail.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2026-01-30 13:35:10 -05:00
scobert969
caae6bc597 Change canned message recipient's previous page to send page (#9227)
* Change canned message recipient's previous page to send page

* Set previousPage for new menu pages
Set nextPage in back MenuPages to previousPage
Removed back MenuAction

---------

Co-authored-by: zeropt <ferr0fluidmann@gmail.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2026-01-30 13:31:26 -05:00
Jonathan Bennett
200e79e800 You get an RTC, and you get an RTC! (delete HAS_RTC as it wasn't actually doing much) (#9493) 2026-01-30 11:54:49 -06:00
Ben Meadors
c19fc62683 Merge pull request #9492 from meshtastic/master
Master to develop
2026-01-30 10:46:11 -06:00
Jonathan Bennett
4cf01e7e53 Adjust pin poweroff for Thinknode M6 2026-01-30 10:05:26 -06:00
Jonathan Bennett
ad4b1d9c2b re-enable RTC support on THINKNODE M3 and M6 2026-01-30 09:50:47 -06:00
Ben Meadors
68733a6c51 Fix issue triage workflow by clarifying device log requirements and improving JSON response handling 2026-01-30 06:32:08 -06:00
renovate[bot]
22617076f8 Update meshtastic/device-ui digest to 63967a4 (#9475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 20:19:12 -06:00
renovate[bot]
6f5a7672b4 Update pschatzmann_arduino-audio-driver to v0.2.1 (#9398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 20:18:34 -06:00
Ben Meadors
e08c050720 Add custom ringtone definition for RAK4631 and enable buzzer pin (#9481) 2026-01-29 20:15:08 -06:00
Ben Meadors
28b4f37a93 Revert "Update libch341-spi-userspace digest to af9bc27 (#9472)" (#9483)
This reverts commit b18742c211.
2026-01-29 20:14:50 -06:00
Jonathan Bennett
5dd06edd00 Add ledOff if not defined 2026-01-29 13:24:10 -06:00
Jonathan Bennett
eeb7373043 Remove errant symbol 2026-01-29 13:23:49 -06:00
Jonathan Bennett
dbded86dcb More variant.h cleanup. LED_NOTIFICATION, remove dead code, etc (#9477) 2026-01-29 12:51:48 -06:00
Jonathan Bennett
45fbc0f9d3 Remove stale variant.h defines (#9470)
* Remove noop CANNED_MESSAGE_MODULE_ENABLE define

* Remove over-eager warning removal

* Remove unused LED_CONN

* Dead defines removal

* Rename oddball LED pin name

* Rename second oddball LED pin name

* Remove another dead define
2026-01-29 10:58:06 -06:00
Agustín Mista
61b39acc7d Add initial Nix shell (#8530)
* Add initial Nix shell

* Update flake.nix

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-29 10:06:58 -06:00
Jorropo
8af9e7fbdc enable long interleaving mode for LR11x0 and SX128x (#9399)
Using long interleaving is not a breaking change, the receiver node is able
to use the lora header to know if LI encoding is used or not and will
decode LI packets correctly.

However the problem is SX127x and other first generation LoRa IP which do not
support LI at all, for theses it is a breaking change.

HOWEVER due to the sync word bug the LR11x0 already can't talk with SX127x,
so if we enable LI on theses no one would be able to tell.
Same for SX128x altho this is because it works on 2.4Ghz which is incompatible with SX127x.

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-29 06:44:17 -06:00
Andrew Yong
1f7ed6888a feat(stm32): Add Milesight GS301 Bathroom Odor Detector (#9359)
- STM32WLE5CCU6
- NFC (unsupported): NXP NT3H2211W0FTTJ (NTAG I2C plus: NFC Forum T2T with I2C interface, password protection and energy harvesting)
- Sensor (unsupported): Analog ADuCM355 (SHTC3 is connected to ADuCM355 and not directly accessible)
- Bicolor LED
- User button (presently not functional in STM32 variants)

The definitions for sensor voltage control are present but commented out to save power, due to lack of sensor support.

Powered by 4x 4000mAh RAMWAY ER18505 Li-SOCl2 batteries.

Flashing:

1. Power down device (remove batteries)
2. Connect USB-UART to J1 (USART2), pinout is below, do not connect +3V3 pin yet
3. Short BOOT pins next to J1
4. Connect +3V3 pin or insert batteries while BOOT pins are shorted
5. Use STM32CubeProgrammer, connect by UART mode
6. Load firmware .hex and download

J1 (USART2); Molex Picoblade (P=1.25mm * 4)

1. +3V3
2. PA3_USART2_RX_J1
3. PA2_USART2_TX_J1
4. GND

Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-29 06:43:48 -06:00
treysis
31bf51b3f2 Add support for the hardware buttons on Bluetooth Nugget device (#9468)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-29 06:38:49 -06:00
Justin E. Mann
334a4f04cd Fix logic for rak12035 sensor default config and improve messaging (#9414)
* better logic to check if the RAK12035 soil sensor is calibrated, better log messaging if either of the default values were used.

* .

* changes to how default calibration is done and a message it the default calibration is used pointing to the actual calibration sketch so the user can find it and use it to improve accuracy.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-29 06:29:15 -06:00
214 changed files with 3402 additions and 1081 deletions

1
.envrc Normal file
View File

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

View File

@@ -100,7 +100,12 @@ jobs:
prompt: |
Analyze this GitHub issue for completeness and determine if it needs labels.
If this looks like a bug on the device/firmware (crash, reboot, lockup, radio issues, GPS issues, display issues, power/sleep issues), request device logs and explain how to get them:
IMPORTANT: Distinguish between:
- Device/firmware bugs (crashes, reboots, lockups, radio/GPS/display/power issues) - these need device logs
- Build/release/packaging issues (missing files, CI failures, download problems) - these do NOT need device logs
- Documentation or website issues - these do NOT need device logs
If this is a device/firmware bug, request device logs and explain how to get them:
Web Flasher logs:
- Go to https://flasher.meshtastic.org
@@ -113,20 +118,18 @@ jobs:
Also request key context if missing: device model/variant, firmware version, region, steps to reproduce, expected vs actual.
Respond ONLY with JSON:
{
"complete": true|false,
"comment": "Your helpful comment requesting missing info, or empty string if complete",
"label": "needs-logs" | "needs-info" | "none"
}
Respond ONLY with valid JSON (no markdown, no code fences):
{"complete": true, "comment": "", "label": "none"}
OR
{"complete": false, "comment": "Your helpful comment", "label": "needs-logs"}
Use "needs-logs" if this is a device bug AND no logs are attached.
Use "needs-logs" ONLY if this is a device/firmware bug AND no logs are attached.
Use "needs-info" if basic info like firmware version or steps to reproduce are missing.
Use "none" if the issue is complete or is a feature request.
Use "none" if the issue is complete, is a feature request, or is a build/CI/packaging issue.
Title: ${{ github.event.issue.title }}
Body: ${{ github.event.issue.body }}
system-prompt: You are a helpful assistant that triages GitHub issues. Be conservative with labels.
system-prompt: You are a helpful assistant that triages GitHub issues. Be conservative with labels. Only request device logs for actual device/firmware bugs, not for build/release/CI issues.
model: openai/gpt-4o-mini
- name: Process analysis result
@@ -137,9 +140,12 @@ jobs:
AI_RESPONSE: ${{ steps.analysis.outputs.response }}
with:
script: |
const raw = (process.env.AI_RESPONSE || '').trim();
let raw = (process.env.AI_RESPONSE || '').trim();
let complete = false;
// Strip markdown code fences if present
raw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
let complete = true;
let comment = '';
let label = 'none';
@@ -149,9 +155,10 @@ jobs:
comment = (parsed.comment ?? '').toString().trim();
label = (parsed.label ?? 'none').toString().trim().toLowerCase();
} catch {
// If JSON parse fails, treat as incomplete with raw response as comment
complete = false;
comment = raw;
// If JSON parse fails, log warning and don't comment (avoid posting raw JSON)
console.log('Failed to parse AI response as JSON:', raw);
complete = true;
comment = '';
label = 'none';
}
@@ -159,7 +166,9 @@ jobs:
const allowedLabels = new Set(['needs-logs', 'needs-info', 'none']);
if (!allowedLabels.has(label)) label = 'none';
core.setOutput('should_comment', (!complete && comment.length > 0) ? 'true' : 'false');
// Only comment if we have a valid parsed comment (not raw JSON)
const shouldComment = !complete && comment.length > 0 && !comment.startsWith('{');
core.setOutput('should_comment', shouldComment ? 'true' : 'false');
core.setOutput('comment_body', comment);
core.setOutput('label', label);

3
.gitignore vendored
View File

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

View File

@@ -8,18 +8,18 @@ plugins:
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- checkov@3.2.497
- renovate@42.84.2
- prettier@3.8.0
- trufflehog@3.92.5
- checkov@3.2.500
- renovate@43.4.0
- prettier@3.8.1
- trufflehog@3.93.0
- yamllint@1.38.0
- bandit@1.9.3
- trivy@0.68.2
- trivy@0.69.1
- taplo@0.10.0
- ruff@0.14.13
- ruff@0.15.0
- isort@7.0.0
- markdownlint@0.47.0
- oxipng@10.0.0
- oxipng@10.1.0
- svgo@4.0.0
- actionlint@1.7.10
- flake8@7.3.0

View File

@@ -156,16 +156,8 @@ IF %BPS_RESET% EQU 1 (
SET "PROGNAME=!FILENAME:.factory.bin=!"
CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
IF "__!MCU!__" == "__esp32s3__" (
@REM We are working with ESP32-S3
SET "OTA_FILENAME=bleota-s3.bin"
) ELSE IF "__!MCU!__" == "__esp32c3__" (
@REM We are working with ESP32-C3
SET "OTA_FILENAME=bleota-c3.bin"
) ELSE (
@REM Everything else
SET "OTA_FILENAME=bleota.bin"
)
@REM Determine OTA filename based on MCU type (unified OTA format)
SET "OTA_FILENAME=mt-!MCU!-ota.bin"
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
@REM Set SPIFFS filename with "littlefs-" prefix.

View File

@@ -131,14 +131,8 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
exit 1
fi
# Determine OTA filename based on MCU type
if [ "$MCU" == "esp32s3" ]; then
OTAFILE=bleota-s3.bin
elif [ "$MCU" == "esp32c3" ]; then
OTAFILE=bleota-c3.bin
else
OTAFILE=bleota.bin
fi
# Determine OTA filename based on MCU type (unified OTA format)
OTAFILE="mt-${MCU}-ota.bin"
# Set SPIFFS filename with "littlefs-" prefix.
SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"

44
flake.lock generated Normal file
View File

@@ -0,0 +1,44 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1766314097,
"narHash": "sha256-laJftWbghBehazn/zxVJ8NdENVgjccsWAdAqKXhErrM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "306ea70f9eb0fb4e040f8540e2deab32ed7e2055",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

66
flake.nix Normal file
View File

@@ -0,0 +1,66 @@
{
description = "Nix flake to compile Meshtastic firmware";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
# Shim to make flake.nix work with stable Nix.
flake-compat = {
url = "github:NixOS/flake-compat";
flake = false;
};
};
outputs =
inputs:
let
lib = inputs.nixpkgs.lib;
forAllSystems =
fn:
lib.genAttrs lib.systems.flakeExposed (
system:
fn {
pkgs = import inputs.nixpkgs {
inherit system;
};
inherit system;
}
);
in
{
devShells = forAllSystems (
{ pkgs, ... }:
let
python3 = pkgs.python312.withPackages (
ps: with ps; [
google
]
);
in
{
default = pkgs.mkShell {
buildInputs = with pkgs; [
python3
platformio
];
shellHook = ''
# Set up PlatformIO to use a local core directory.
export PLATFORMIO_CORE_DIR=$PWD/.platformio
# Tell pip to put packages into $PIP_PREFIX instead of the usual
# location. This is especially necessary under NixOS to avoid having
# pip trying to write to the read-only Nix store. For more info,
# see https://wiki.nixos.org/wiki/Python
export PIP_PREFIX=$PWD/.python3
export PYTHONPATH="$PIP_PREFIX/${python3.sitePackages}"
export PATH="$PIP_PREFIX/bin:$PATH"
# Avoids reproducibility issues with some Python packages
# See https://nixos.org/manual/nixpkgs/stable/#python-setup.py-bdist_wheel-cannot-create-.whl
unset SOURCE_DATE_EPOCH
'';
};
}
);
};
}

View File

@@ -66,7 +66,7 @@ monitor_speed = 115200
monitor_filters = direct
lib_deps =
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/b34c6817c25d6faabb3a8a162b5d14fb75395433.zip
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/21e484f409cde18d44012caef84c244eb5ca28f3.zip
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
@@ -120,7 +120,7 @@ lib_deps =
[device-ui_base]
lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/69739b84f87a91568d3c421498bc89977937a141.zip
https://github.com/meshtastic/device-ui/archive/6c75195e9987b7a49563232234f2f868dd343cae.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]
@@ -144,7 +144,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
adafruit/Adafruit INA219@1.2.3
# renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050
adafruit/Adafruit MPU6050@2.2.6
adafruit/Adafruit MPU6050@2.2.8
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH
adafruit/Adafruit LIS3DH@1.3.0
# renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0
@@ -213,6 +213,7 @@ lib_deps =
sensirion/Sensirion Core@0.7.2
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
[environmental_extra_no_bsec]
lib_deps =
@@ -239,4 +240,4 @@ lib_deps =
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
sensirion/Sensirion Core@0.7.2
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0
sensirion/Sensirion I2C SCD4x@1.1.0

12
shell.nix Normal file
View File

@@ -0,0 +1,12 @@
(import (
let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
nodeName = lock.nodes.root.inputs.flake-compat;
in
fetchTarball {
url =
lock.nodes.${nodeName}.locked.url
or "https://github.com/NixOS/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz";
sha256 = lock.nodes.${nodeName}.locked.narHash;
}
) { src = ./.; }).shellNix

View File

@@ -816,6 +816,9 @@ void Power::shutdown()
#endif
#ifdef PIN_LED3
ledOff(PIN_LED3);
#endif
#ifdef LED_NOTIFICATION
ledOff(LED_NOTIFICATION);
#endif
doDeepSleep(DELAY_FOREVER, true, true);
#elif defined(ARCH_PORTDUINO)

View File

@@ -241,6 +241,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define BQ27220_ADDR 0x55 // same address as TDECK_KB
#define BQ25896_ADDR 0x6B
#define LTR553ALS_ADDR 0x23
#define SEN5X_ADDR 0x69
// -----------------------------------------------------------------------------
// ACCELEROMETER
@@ -390,9 +391,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#ifndef HAS_RADIO
#define HAS_RADIO 0
#endif
#ifndef HAS_RTC
#define HAS_RTC 0
#endif
#ifndef HAS_CPU_SHUTDOWN
#define HAS_CPU_SHUTDOWN 0
#endif
@@ -428,12 +426,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define HAS_RGB_LED
#endif
#ifndef LED_STATE_OFF
#define LED_STATE_OFF 0
#endif
#ifndef LED_STATE_ON
#define LED_STATE_ON 1
#endif
#ifndef LED_STATE_OFF
#define LED_STATE_OFF (LED_STATE_ON ^ 1)
#endif
#ifndef ledOff
#define ledOff(pin) pinMode(pin, INPUT)
#endif
// default mapping of pins
#if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN)

View File

@@ -88,7 +88,8 @@ class ScanI2C
BH1750,
DA217,
CHSC6X,
CST226SE
CST226SE,
SEN5X
} DeviceType;
// typedef uint8_t DeviceAddress;

View File

@@ -8,6 +8,7 @@
#endif
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
#include "meshUtils.h" // vformat
#endif
bool in_array(uint8_t *array, int size, uint8_t lookfor)
@@ -114,6 +115,45 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
return value;
}
/// for SEN5X detection
// Note, this code needs to be called before setting the I2C bus speed
// for the screen at high speed. The speed needs to be at 100kHz, otherwise
// detection will not work
String readSEN5xProductName(TwoWire *i2cBus, uint8_t address)
{
uint8_t cmd[] = {0xD0, 0x14};
uint8_t response[48] = {0};
i2cBus->beginTransmission(address);
i2cBus->write(cmd, 2);
if (i2cBus->endTransmission() != 0)
return "";
delay(20);
if (i2cBus->requestFrom(address, (uint8_t)48) != 48)
return "";
for (int i = 0; i < 48 && i2cBus->available(); ++i) {
response[i] = i2cBus->read();
}
char productName[33] = {0};
int j = 0;
for (int i = 0; i < 48 && j < 32; i += 3) {
if (response[i] >= 32 && response[i] <= 126)
productName[j++] = response[i];
else
break;
if (response[i + 1] >= 32 && response[i + 1] <= 126)
productName[j++] = response[i + 1];
else
break;
}
return String(productName);
}
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
case ADDR: \
logFoundDevice(__VA_ARGS__); \
@@ -568,8 +608,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
}
break;
case ICM20948_ADDR: // same as BMX160_ADDR
case ICM20948_ADDR: // same as BMX160_ADDR and SEN5X_ADDR
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
// ICM20948 Register check
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
#ifdef HAS_ICM20948
type = ICM20948;
@@ -580,14 +621,31 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
type = ICM20948;
logFoundDevice("ICM20948", (uint8_t)addr.address);
break;
} else if (addr.address == BMX160_ADDR) {
type = BMX160;
logFoundDevice("BMX160", (uint8_t)addr.address);
break;
} else {
type = MPU6050;
logFoundDevice("MPU6050", (uint8_t)addr.address);
break;
String prod = "";
prod = readSEN5xProductName(i2cBus, addr.address);
if (prod.startsWith("SEN55")) {
type = SEN5X;
logFoundDevice("Sensirion SEN55", addr.address);
break;
} else if (prod.startsWith("SEN54")) {
type = SEN5X;
logFoundDevice("Sensirion SEN54", addr.address);
break;
} else if (prod.startsWith("SEN50")) {
type = SEN5X;
logFoundDevice("Sensirion SEN50", addr.address);
break;
}
if (addr.address == BMX160_ADDR) {
type = BMX160;
logFoundDevice("BMX160", (uint8_t)addr.address);
break;
} else {
type = MPU6050;
logFoundDevice("MPU6050", (uint8_t)addr.address);
break;
}
}
break;

31
src/detect/reClockI2C.cpp Normal file
View File

@@ -0,0 +1,31 @@
#include "reClockI2C.h"
#include "ScanI2CTwoWire.h"
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force)
{
uint32_t currentClock = 0;
/* See https://github.com/arduino/Arduino/issues/11457
Currently, only ESP32 can getClock()
While all cores can setClock()
https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50
https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60
https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103
For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes)
we need to reclock I2C and set it back to the previous desired speed.
Only for cases where we can know OR predefine the speed, we can do this.
*/
// TODO add getClock function or return a predefined clock speed per variant?
#ifdef CAN_RECLOCK_I2C
currentClock = i2cBus->getClock();
#endif
if ((currentClock != desiredClock) || force) {
LOG_DEBUG("Changing I2C clock to %u", desiredClock);
i2cBus->setClock(desiredClock);
}
return currentClock;
}

View File

@@ -1,41 +1,11 @@
#ifdef CAN_RECLOCK_I2C
#ifndef RECLOCK_I2C_
#define RECLOCK_I2C_
#include "ScanI2CTwoWire.h"
#include <Wire.h>
#include <stdint.h>
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus)
{
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force);
uint32_t currentClock;
/* See https://github.com/arduino/Arduino/issues/11457
Currently, only ESP32 can getClock()
While all cores can setClock()
https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50
https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60
https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103
For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes)
we need to reclock I2C and set it back to the previous desired speed.
Only for cases where we can know OR predefine the speed, we can do this.
*/
#ifdef ARCH_ESP32
currentClock = i2cBus->getClock();
#elif defined(ARCH_NRF52)
// TODO add getClock function or return a predefined clock speed per variant?
return 0;
#elif defined(ARCH_RP2040)
// TODO add getClock function or return a predefined clock speed per variant
return 0;
#elif defined(ARCH_STM32WL)
// TODO add getClock function or return a predefined clock speed per variant
return 0;
#else
return 0;
#endif
if (currentClock != desiredClock) {
LOG_DEBUG("Changing I2C clock to %u", desiredClock);
i2cBus->setClock(desiredClock);
}
return currentClock;
}
#endif

View File

@@ -72,11 +72,13 @@ RTCSetResult readFromRTC()
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
#if defined(PCF8563_RTC)
if (rtc_found.address == PCF8563_RTC) {
SensorPCF8563 rtc;
#elif defined(PCF85063_RTC)
if (rtc_found.address == PCF85063_RTC) {
SensorPCF85063 rtc;
#endif
uint32_t now = millis();
SensorRtcHelper rtc;
#if WIRE_INTERFACES_COUNT == 2
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
@@ -240,10 +242,12 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
#if defined(PCF8563_RTC)
if (rtc_found.address == PCF8563_RTC) {
SensorPCF8563 rtc;
#elif defined(PCF85063_RTC)
if (rtc_found.address == PCF85063_RTC) {
SensorPCF85063 rtc;
#endif
SensorRtcHelper rtc;
#if WIRE_INTERFACES_COUNT == 2
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
@@ -276,11 +280,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
settimeofday(tv, NULL);
#endif
// nrf52 doesn't have a readable RTC (yet - software not written)
#if HAS_RTC
readFromRTC();
#endif
return RTCSetResultSuccess;
} else {
return RTCSetResultNotSet; // RTC was already set with a higher quality time

View File

@@ -58,7 +58,7 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp
} // namespace
menuHandler::screenMenus menuHandler::menuQueue = menu_none;
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
uint32_t menuHandler::pickedNodeNum = 0;
bool test_enabled = false;
uint8_t test_count = 0;
@@ -66,7 +66,7 @@ uint8_t test_count = 0;
void menuHandler::loraMenu()
{
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"};
enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, frequency_slot = 3, lora_picker = 4 };
enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 };
BannerOverlayOptions bannerOptions;
bannerOptions.message = "LoRa Actions";
bannerOptions.optionsArrayPtr = optionsArray;
@@ -74,14 +74,14 @@ void menuHandler::loraMenu()
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
// No action
} else if (selected == device_role_picker) {
menuHandler::menuQueue = menuHandler::device_role_picker;
} else if (selected == radio_preset_picker) {
menuHandler::menuQueue = menuHandler::radio_preset_picker;
} else if (selected == frequency_slot) {
menuHandler::menuQueue = menuHandler::frequency_slot;
} else if (selected == lora_picker) {
menuHandler::menuQueue = menuHandler::lora_picker;
} else if (selected == DeviceRolePicker) {
menuHandler::menuQueue = menuHandler::DeviceRolePicker;
} else if (selected == RadioPresetPicker) {
menuHandler::menuQueue = menuHandler::RadioPresetPicker;
} else if (selected == FrequencySlot) {
menuHandler::menuQueue = menuHandler::FrequencySlot;
} else if (selected == LoraPicker) {
menuHandler::menuQueue = menuHandler::LoraPicker;
}
};
screen->showOverlayBanner(bannerOptions);
@@ -102,7 +102,7 @@ void menuHandler::OnboardMessage()
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
menuHandler::menuQueue = menuHandler::no_timeout_lora_picker;
menuHandler::menuQueue = menuHandler::NoTimeoutLoraPicker;
screen->runNow();
};
screen->showOverlayBanner(bannerOptions);
@@ -216,7 +216,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::DeviceRolePicker()
void menuHandler::deviceRolePicker()
{
static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
enum optionsNumbers {
@@ -232,7 +232,7 @@ void menuHandler::DeviceRolePicker()
bannerOptions.optionsCount = 5;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::lora_Menu;
menuHandler::menuQueue = menuHandler::LoraMenu;
screen->runNow();
return;
} else if (selected == devicerole_client) {
@@ -300,7 +300,7 @@ void menuHandler::FrequencySlotPicker()
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::lora_Menu;
menuHandler::menuQueue = menuHandler::LoraMenu;
screen->runNow();
return;
}
@@ -313,7 +313,7 @@ void menuHandler::FrequencySlotPicker()
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::RadioPresetPicker()
void menuHandler::radioPresetPicker()
{
static const RadioPresetOption presetOptions[] = {
{"Back", OptionsAction::Back},
@@ -333,7 +333,7 @@ void menuHandler::RadioPresetPicker()
auto bannerOptions =
createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuHandler::menuQueue = menuHandler::lora_Menu;
menuHandler::menuQueue = menuHandler::LoraMenu;
screen->runNow();
return;
}
@@ -352,7 +352,7 @@ void menuHandler::RadioPresetPicker()
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::TwelveHourPicker()
void menuHandler::twelveHourPicker()
{
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 };
@@ -362,7 +362,7 @@ void menuHandler::TwelveHourPicker()
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::clock_menu;
menuHandler::menuQueue = menuHandler::ClockMenu;
screen->runNow();
} else if (selected == twelve) {
config.display.use_12h_clock = true;
@@ -390,7 +390,7 @@ void menuHandler::showConfirmationBanner(const char *message, std::function<void
screen->showOverlayBanner(confirmBanner);
}
void menuHandler::ClockFacePicker()
void menuHandler::clockFacePicker()
{
static const ClockFaceOption clockFaceOptions[] = {
{"Back", OptionsAction::Back},
@@ -404,7 +404,7 @@ void menuHandler::ClockFacePicker()
auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels,
[](const ClockFaceOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuHandler::menuQueue = menuHandler::clock_menu;
menuHandler::menuQueue = menuHandler::ClockMenu;
screen->runNow();
return;
}
@@ -456,7 +456,7 @@ void menuHandler::TZPicker()
auto bannerOptions = createStaticBannerOptions(
"Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuHandler::menuQueue = menuHandler::clock_menu;
menuHandler::menuQueue = menuHandler::ClockMenu;
screen->runNow();
return;
}
@@ -503,13 +503,13 @@ void menuHandler::clockMenu()
bannerOptions.optionsCount = 4;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Clock) {
menuHandler::menuQueue = menuHandler::clock_face_picker;
menuHandler::menuQueue = menuHandler::ClockFacePicker;
screen->runNow();
} else if (selected == Time) {
menuHandler::menuQueue = menuHandler::twelve_hour_picker;
menuHandler::menuQueue = menuHandler::TwelveHourPicker;
screen->runNow();
} else if (selected == Timezone) {
menuHandler::menuQueue = menuHandler::TZ_picker;
menuHandler::menuQueue = menuHandler::TzPicker;
screen->runNow();
}
};
@@ -572,12 +572,12 @@ void menuHandler::messageResponseMenu()
LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer);
if (selected == ViewMode) {
menuHandler::menuQueue = menuHandler::message_viewmode_menu;
menuHandler::menuQueue = menuHandler::MessageViewModeMenu;
screen->runNow();
// Reply submenu
} else if (selected == ReplyMenu) {
menuHandler::menuQueue = menuHandler::reply_menu;
menuHandler::menuQueue = menuHandler::ReplyMenu;
screen->runNow();
} else if (selected == MuteChannel) {
@@ -589,7 +589,7 @@ void menuHandler::messageResponseMenu()
}
} else if (selected == DeleteMenu) {
menuHandler::menuQueue = menuHandler::delete_messages_menu;
menuHandler::menuQueue = menuHandler::DeleteMessagesMenu;
screen->runNow();
#ifdef HAS_I2S
@@ -649,7 +649,7 @@ void menuHandler::replyMenu()
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (selected == Back) {
menuHandler::menuQueue = menuHandler::message_response_menu;
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
screen->runNow();
return;
}
@@ -737,7 +737,7 @@ void menuHandler::deleteMessagesMenu()
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (selected == Back) {
menuHandler::menuQueue = menuHandler::message_response_menu;
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
screen->runNow();
return;
}
@@ -901,7 +901,7 @@ void menuHandler::messageViewModeMenu()
bannerOptions.bannerCallback = [=](int selected) -> void {
LOG_DEBUG("messageViewModeMenu: selected=%d", selected);
if (selected == -1) {
menuHandler::menuQueue = menuHandler::message_response_menu;
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
screen->runNow();
} else if (selected == -2) {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL);
@@ -1083,23 +1083,23 @@ void menuHandler::systemBaseMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Notifications) {
menuHandler::menuQueue = menuHandler::buzzermodemenupicker;
menuHandler::menuQueue = menuHandler::BuzzerModeMenuPicker;
screen->runNow();
} else if (selected == ScreenOptions) {
menuHandler::menuQueue = menuHandler::screen_options_menu;
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
screen->runNow();
} else if (selected == PowerMenu) {
menuHandler::menuQueue = menuHandler::power_menu;
menuHandler::menuQueue = menuHandler::PowerMenu;
screen->runNow();
} else if (selected == Test) {
menuHandler::menuQueue = menuHandler::test_menu;
menuHandler::menuQueue = menuHandler::TestMenu;
screen->runNow();
} else if (selected == Bluetooth) {
menuQueue = bluetooth_toggle_menu;
menuQueue = BluetoothToggleMenu;
screen->runNow();
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
} else if (selected == WiFiToggle) {
menuQueue = wifi_toggle_menu;
menuQueue = WifiToggleMenu;
screen->runNow();
#endif
} else if (selected == Back && !test_enabled) {
@@ -1177,7 +1177,7 @@ void menuHandler::favoriteBaseMenu()
evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
screen->handleUIFrameEvent(&evt);
} else if (selected == Remove) {
menuHandler::menuQueue = menuHandler::remove_favorite;
menuHandler::menuQueue = menuHandler::RemoveFavorite;
screen->runNow();
} else if (selected == TraceRoute) {
if (traceRouteModule) {
@@ -1238,15 +1238,15 @@ void menuHandler::positionBaseMenu()
auto action = static_cast<PositionAction>(option.value);
switch (action) {
case PositionAction::GpsToggle:
menuQueue = gps_toggle_menu;
menuQueue = GpsToggleMenu;
screen->runNow();
break;
case PositionAction::GpsFormat:
menuQueue = gps_format_menu;
menuQueue = GpsFormatMenu;
screen->runNow();
break;
case PositionAction::CompassMenu:
menuQueue = compass_point_north_menu;
menuQueue = CompassPointNorthMenu;
screen->runNow();
break;
case PositionAction::CompassCalibrate:
@@ -1255,15 +1255,15 @@ void menuHandler::positionBaseMenu()
}
break;
case PositionAction::GPSSmartPosition:
menuQueue = gps_smart_position_menu;
menuQueue = GpsSmartPositionMenu;
screen->runNow();
break;
case PositionAction::GPSUpdateInterval:
menuQueue = gps_update_interval_menu;
menuQueue = GpsUpdateIntervalMenu;
screen->runNow();
break;
case PositionAction::GPSPositionBroadcast:
menuQueue = gps_position_broadcast_menu;
menuQueue = GpsPositionBroadcastMenu;
screen->runNow();
break;
}
@@ -1303,13 +1303,13 @@ void menuHandler::nodeListMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == NodePicker) {
menuQueue = NodePicker_menu;
menuQueue = NodePickerMenu;
screen->runNow();
} else if (selected == Reset) {
menuQueue = reset_node_db_menu;
menuQueue = ResetNodeDbMenu;
screen->runNow();
} else if (selected == NodeNameLength) {
menuHandler::menuQueue = menuHandler::node_name_length_menu;
menuHandler::menuQueue = menuHandler::NodeNameLengthMenu;
screen->runNow();
}
};
@@ -1330,12 +1330,12 @@ void menuHandler::NodePicker()
menuHandler::pickedNodeNum = nodenum;
// Keep UI favorite context in sync (used elsewhere for some node-based actions)
graphics::UIRenderer::currentFavoriteNodeNum = nodenum;
menuQueue = Manage_Node_menu;
menuQueue = ManageNodeMenu;
screen->runNow();
});
}
void menuHandler::ManageNodeMenu()
void menuHandler::manageNodeMenu()
{
// If we don't have a node selected yet, go fast exit
auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
@@ -1391,7 +1391,7 @@ void menuHandler::ManageNodeMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuQueue = node_base_menu;
menuQueue = NodeBaseMenu;
screen->runNow();
return;
}
@@ -1483,7 +1483,7 @@ void menuHandler::nodeNameLengthMenu()
auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels,
[](const NodeNameOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = node_base_menu;
menuQueue = NodeBaseMenu;
screen->runNow();
return;
}
@@ -1498,6 +1498,7 @@ void menuHandler::nodeNameLengthMenu()
config.display.use_long_node_name = option.value;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
LOG_INFO("Setting names to %s", option.value ? "long" : "short");
});
@@ -1528,7 +1529,7 @@ void menuHandler::resetNodeDBMenu()
nodeDB->resetNodes(1);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
} else if (selected == 0) {
menuQueue = node_base_menu;
menuQueue = NodeBaseMenu;
screen->runNow();
}
};
@@ -1550,7 +1551,7 @@ void menuHandler::compassNorthMenu()
auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels,
[](const CompassOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = position_base_menu;
menuQueue = PositionBaseMenu;
screen->runNow();
return;
}
@@ -1595,7 +1596,7 @@ void menuHandler::GPSToggleMenu()
auto bannerOptions =
createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = position_base_menu;
menuQueue = PositionBaseMenu;
screen->runNow();
return;
}
@@ -1660,7 +1661,7 @@ void menuHandler::GPSFormatMenu()
auto onSelection = [](const GPSFormatOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = position_base_menu;
menuQueue = PositionBaseMenu;
screen->runNow();
return;
}
@@ -1715,7 +1716,7 @@ void menuHandler::GPSSmartPositionMenu()
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) {
menuQueue = position_base_menu;
menuQueue = PositionBaseMenu;
screen->runNow();
} else if (selected == 1) {
config.position.position_broadcast_smart_enabled = true;
@@ -1744,7 +1745,7 @@ void menuHandler::GPSUpdateIntervalMenu()
bannerOptions.optionsCount = 16;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) {
menuQueue = position_base_menu;
menuQueue = PositionBaseMenu;
screen->runNow();
} else if (selected == 1) {
config.position.gps_update_interval = 8;
@@ -1832,7 +1833,7 @@ void menuHandler::GPSPositionBroadcastMenu()
bannerOptions.optionsCount = 17;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) {
menuQueue = position_base_menu;
menuQueue = PositionBaseMenu;
screen->runNow();
} else if (selected == 1) {
config.position.position_broadcast_secs = 60;
@@ -1915,7 +1916,7 @@ void menuHandler::GPSPositionBroadcastMenu()
#endif
void menuHandler::BluetoothToggleMenu()
void menuHandler::bluetoothToggleMenu()
{
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
BannerOverlayOptions bannerOptions;
@@ -2043,7 +2044,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
auto bannerOptions = createStaticBannerOptions(
"Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = system_base_menu;
menuQueue = SystemBaseMenu;
screen->runNow();
return;
}
@@ -2138,7 +2139,7 @@ void menuHandler::rebootMenu()
messageStore.saveToFlash();
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
} else {
menuQueue = power_menu;
menuQueue = PowerMenu;
screen->runNow();
}
};
@@ -2160,7 +2161,7 @@ void menuHandler::shutdownMenu()
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
} else {
menuQueue = power_menu;
menuQueue = PowerMenu;
screen->runNow();
}
};
@@ -2221,14 +2222,14 @@ void menuHandler::testMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == NumberPicker) {
menuQueue = number_test;
menuQueue = NumberTest;
screen->runNow();
} else if (selected == ShowChirpy) {
screen->toggleFrameVisibility("chirpy");
screen->setFrames(Screen::FOCUS_SYSTEM);
} else {
menuQueue = system_base_menu;
menuQueue = SystemBaseMenu;
screen->runNow();
}
};
@@ -2252,7 +2253,7 @@ void menuHandler::wifiBaseMenu()
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Wifi_toggle) {
menuQueue = wifi_toggle_menu;
menuQueue = WifiToggleMenu;
screen->runNow();
}
};
@@ -2301,9 +2302,9 @@ void menuHandler::screenOptionsMenu()
hasSupportBrightness = false;
#endif
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits };
static const char *optionsArray[5] = {"Back"};
static int optionsEnumArray[5] = {Back};
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles };
static const char *optionsArray[6] = {"Back"};
static int optionsEnumArray[6] = {Back};
int options = 1;
// Only show brightness for B&W displays
@@ -2325,6 +2326,9 @@ void menuHandler::screenOptionsMenu()
optionsArray[options] = "Display Units";
optionsEnumArray[options++] = DisplayUnits;
optionsArray[options] = "Message Bubbles";
optionsEnumArray[options++] = MessageBubbles;
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Display Options";
bannerOptions.optionsArrayPtr = optionsArray;
@@ -2332,10 +2336,10 @@ void menuHandler::screenOptionsMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Brightness) {
menuHandler::menuQueue = menuHandler::brightness_picker;
menuHandler::menuQueue = menuHandler::BrightnessPicker;
screen->runNow();
} else if (selected == ScreenColor) {
menuHandler::menuQueue = menuHandler::tftcolormenupicker;
menuHandler::menuQueue = menuHandler::TftColorMenuPicker;
screen->runNow();
} else if (selected == FrameToggles) {
menuHandler::menuQueue = menuHandler::FrameToggles;
@@ -2343,8 +2347,11 @@ void menuHandler::screenOptionsMenu()
} else if (selected == DisplayUnits) {
menuHandler::menuQueue = menuHandler::DisplayUnits;
screen->runNow();
} else if (selected == MessageBubbles) {
menuHandler::menuQueue = menuHandler::MessageBubblesMenu;
screen->runNow();
} else {
menuQueue = system_base_menu;
menuQueue = SystemBaseMenu;
screen->runNow();
}
};
@@ -2380,16 +2387,16 @@ void menuHandler::powerMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Reboot) {
menuHandler::menuQueue = menuHandler::reboot_menu;
menuHandler::menuQueue = menuHandler::RebootMenu;
screen->runNow();
} else if (selected == Shutdown) {
menuHandler::menuQueue = menuHandler::shutdown_menu;
menuHandler::menuQueue = menuHandler::ShutdownMenu;
screen->runNow();
} else if (selected == MUI) {
menuHandler::menuQueue = menuHandler::mui_picker;
menuHandler::menuQueue = menuHandler::MuiPicker;
screen->runNow();
} else {
menuQueue = system_base_menu;
menuQueue = SystemBaseMenu;
screen->runNow();
}
};
@@ -2427,7 +2434,7 @@ void menuHandler::keyVerificationFinalPrompt()
}
}
void menuHandler::FrameToggles_menu()
void menuHandler::frameTogglesMenu()
{
enum optionsNumbers {
Finish,
@@ -2571,7 +2578,7 @@ void menuHandler::FrameToggles_menu()
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::DisplayUnits_menu()
void menuHandler::displayUnitsMenu()
{
enum optionsNumbers { Back, MetricUnits, ImperialUnits };
@@ -2592,7 +2599,34 @@ void menuHandler::DisplayUnits_menu()
config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL;
service->reloadConfig(SEGMENT_CONFIG);
} else {
menuHandler::menuQueue = menuHandler::screen_options_menu;
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::messageBubblesMenu()
{
enum optionsNumbers { Back, ShowBubbles, HideBubbles };
static const char *optionsArray[] = {"Back", "Show Bubbles", "Hide Bubbles"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Message Bubbles";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3;
bannerOptions.InitialSelected = config.display.enable_message_bubbles ? 1 : 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == ShowBubbles) {
config.display.enable_message_bubbles = true;
service->reloadConfig(SEGMENT_CONFIG);
LOG_INFO("Message bubbles enabled");
} else if (selected == HideBubbles) {
config.display.enable_message_bubbles = false;
service->reloadConfig(SEGMENT_CONFIG);
LOG_INFO("Message bubbles disabled");
} else {
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
screen->runNow();
}
};
@@ -2601,153 +2635,156 @@ void menuHandler::DisplayUnits_menu()
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
{
if (menuQueue != menu_none)
if (menuQueue != MenuNone)
test_count = 0;
switch (menuQueue) {
case menu_none:
case MenuNone:
break;
case lora_Menu:
case LoraMenu:
loraMenu();
break;
case lora_picker:
case LoraPicker:
LoraRegionPicker();
break;
case device_role_picker:
DeviceRolePicker();
case DeviceRolePicker:
deviceRolePicker();
break;
case radio_preset_picker:
RadioPresetPicker();
case RadioPresetPicker:
radioPresetPicker();
break;
case frequency_slot:
case FrequencySlot:
FrequencySlotPicker();
break;
case no_timeout_lora_picker:
case NoTimeoutLoraPicker:
LoraRegionPicker(0);
break;
case TZ_picker:
case TzPicker:
TZPicker();
break;
case twelve_hour_picker:
TwelveHourPicker();
case TwelveHourPicker:
twelveHourPicker();
break;
case clock_face_picker:
ClockFacePicker();
case ClockFacePicker:
clockFacePicker();
break;
case clock_menu:
case ClockMenu:
clockMenu();
break;
case system_base_menu:
case SystemBaseMenu:
systemBaseMenu();
break;
case position_base_menu:
case PositionBaseMenu:
positionBaseMenu();
break;
case node_base_menu:
case NodeBaseMenu:
nodeListMenu();
break;
#if !MESHTASTIC_EXCLUDE_GPS
case gps_toggle_menu:
case GpsToggleMenu:
GPSToggleMenu();
break;
case gps_format_menu:
case GpsFormatMenu:
GPSFormatMenu();
break;
case gps_smart_position_menu:
case GpsSmartPositionMenu:
GPSSmartPositionMenu();
break;
case gps_update_interval_menu:
case GpsUpdateIntervalMenu:
GPSUpdateIntervalMenu();
break;
case gps_position_broadcast_menu:
case GpsPositionBroadcastMenu:
GPSPositionBroadcastMenu();
break;
#endif
case compass_point_north_menu:
case CompassPointNorthMenu:
compassNorthMenu();
break;
case reset_node_db_menu:
case ResetNodeDbMenu:
resetNodeDBMenu();
break;
case buzzermodemenupicker:
case BuzzerModeMenuPicker:
BuzzerModeMenu();
break;
case mui_picker:
case MuiPicker:
switchToMUIMenu();
break;
case tftcolormenupicker:
case TftColorMenuPicker:
TFTColorPickerMenu(display);
break;
case brightness_picker:
case BrightnessPicker:
BrightnessPickerMenu();
break;
case node_name_length_menu:
case NodeNameLengthMenu:
nodeNameLengthMenu();
break;
case reboot_menu:
case RebootMenu:
rebootMenu();
break;
case shutdown_menu:
case ShutdownMenu:
shutdownMenu();
break;
case NodePicker_menu:
case NodePickerMenu:
NodePicker();
break;
case Manage_Node_menu:
ManageNodeMenu();
case ManageNodeMenu:
manageNodeMenu();
break;
case remove_favorite:
case RemoveFavorite:
removeFavoriteMenu();
break;
case trace_route_menu:
case TraceRouteMenu:
traceRouteMenu();
break;
case test_menu:
case TestMenu:
testMenu();
break;
case number_test:
case NumberTest:
numberTest();
break;
case wifi_toggle_menu:
case WifiToggleMenu:
wifiToggleMenu();
break;
case key_verification_init:
case KeyVerificationInit:
keyVerificationInitMenu();
break;
case key_verification_final_prompt:
case KeyVerificationFinalPrompt:
keyVerificationFinalPrompt();
break;
case bluetooth_toggle_menu:
BluetoothToggleMenu();
case BluetoothToggleMenu:
bluetoothToggleMenu();
break;
case screen_options_menu:
case ScreenOptionsMenu:
screenOptionsMenu();
break;
case power_menu:
case PowerMenu:
powerMenu();
break;
case FrameToggles:
FrameToggles_menu();
frameTogglesMenu();
break;
case DisplayUnits:
DisplayUnits_menu();
displayUnitsMenu();
break;
case throttle_message:
case ThrottleMessage:
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
break;
case message_response_menu:
case MessageResponseMenu:
messageResponseMenu();
break;
case reply_menu:
case ReplyMenu:
replyMenu();
break;
case delete_messages_menu:
case DeleteMessagesMenu:
deleteMessagesMenu();
break;
case message_viewmode_menu:
case MessageViewModeMenu:
messageViewModeMenu();
break;
case MessageBubblesMenu:
messageBubblesMenu();
break;
}
menuQueue = menu_none;
menuQueue = MenuNone;
}
void menuHandler::saveUIConfig()

View File

@@ -8,53 +8,54 @@ class menuHandler
{
public:
enum screenMenus {
menu_none,
lora_Menu,
lora_picker,
device_role_picker,
radio_preset_picker,
frequency_slot,
no_timeout_lora_picker,
TZ_picker,
twelve_hour_picker,
clock_face_picker,
clock_menu,
position_base_menu,
node_base_menu,
gps_toggle_menu,
gps_format_menu,
gps_smart_position_menu,
gps_update_interval_menu,
gps_position_broadcast_menu,
compass_point_north_menu,
reset_node_db_menu,
buzzermodemenupicker,
mui_picker,
tftcolormenupicker,
brightness_picker,
reboot_menu,
shutdown_menu,
NodePicker_menu,
Manage_Node_menu,
remove_favorite,
test_menu,
number_test,
wifi_toggle_menu,
bluetooth_toggle_menu,
screen_options_menu,
power_menu,
system_base_menu,
key_verification_init,
key_verification_final_prompt,
trace_route_menu,
throttle_message,
message_response_menu,
message_viewmode_menu,
reply_menu,
delete_messages_menu,
node_name_length_menu,
MenuNone,
LoraMenu,
LoraPicker,
DeviceRolePicker,
RadioPresetPicker,
FrequencySlot,
NoTimeoutLoraPicker,
TzPicker,
TwelveHourPicker,
ClockFacePicker,
ClockMenu,
PositionBaseMenu,
NodeBaseMenu,
GpsToggleMenu,
GpsFormatMenu,
GpsSmartPositionMenu,
GpsUpdateIntervalMenu,
GpsPositionBroadcastMenu,
CompassPointNorthMenu,
ResetNodeDbMenu,
BuzzerModeMenuPicker,
MuiPicker,
TftColorMenuPicker,
BrightnessPicker,
RebootMenu,
ShutdownMenu,
NodePickerMenu,
ManageNodeMenu,
RemoveFavorite,
TestMenu,
NumberTest,
WifiToggleMenu,
BluetoothToggleMenu,
ScreenOptionsMenu,
PowerMenu,
SystemBaseMenu,
KeyVerificationInit,
KeyVerificationFinalPrompt,
TraceRouteMenu,
ThrottleMessage,
MessageResponseMenu,
MessageViewModeMenu,
ReplyMenu,
DeleteMessagesMenu,
NodeNameLengthMenu,
FrameToggles,
DisplayUnits
DisplayUnits,
MessageBubblesMenu
};
static screenMenus menuQueue;
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
@@ -62,15 +63,15 @@ class menuHandler
static void OnboardMessage();
static void LoraRegionPicker(uint32_t duration = 30000);
static void loraMenu();
static void DeviceRolePicker();
static void RadioPresetPicker();
static void deviceRolePicker();
static void radioPresetPicker();
static void FrequencySlotPicker();
static void handleMenuSwitch(OLEDDisplay *display);
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
static void clockMenu();
static void TZPicker();
static void TwelveHourPicker();
static void ClockFacePicker();
static void twelveHourPicker();
static void clockFacePicker();
static void messageResponseMenu();
static void messageViewModeMenu();
static void replyMenu();
@@ -95,7 +96,7 @@ class menuHandler
static void rebootMenu();
static void shutdownMenu();
static void NodePicker();
static void ManageNodeMenu();
static void manageNodeMenu();
static void addFavoriteMenu();
static void removeFavoriteMenu();
static void traceRouteMenu();
@@ -106,15 +107,16 @@ class menuHandler
static void screenOptionsMenu();
static void powerMenu();
static void nodeNameLengthMenu();
static void FrameToggles_menu();
static void DisplayUnits_menu();
static void frameTogglesMenu();
static void displayUnitsMenu();
static void messageBubblesMenu();
static void textMessageMenu();
private:
static void saveUIConfig();
static void keyVerificationInitMenu();
static void keyVerificationFinalPrompt();
static void BluetoothToggleMenu();
static void bluetoothToggleMenu();
};
/* Generic Menu Options designations */

View File

@@ -527,8 +527,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
constexpr int BUBBLE_MIN_W = 24;
constexpr int BUBBLE_TEXT_INDENT = 2;
// Check if bubbles are enabled
const bool showBubbles = config.display.enable_message_bubbles;
const int textIndent = showBubbles ? (BUBBLE_PAD_X + BUBBLE_TEXT_INDENT) : LEFT_MARGIN;
// Derived widths
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (BUBBLE_PAD_X * 2);
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (showBubbles ? (BUBBLE_PAD_X * 2) : 0);
const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH;
// Title string depending on mode
@@ -796,114 +800,105 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
}
}
// Draw bubbles
for (size_t bi = 0; bi < blocks.size(); ++bi) {
const auto &b = blocks[bi];
if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end)
continue;
// Draw bubbles (only if enabled)
if (showBubbles) {
for (size_t bi = 0; bi < blocks.size(); ++bi) {
const auto &b = blocks[bi];
if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end)
continue;
int visualTop = lineTop[b.start];
int visualTop = lineTop[b.start];
int topY;
if (isHeader[b.start]) {
// Header start
constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2
topY = visualTop - BUBBLE_PAD_TOP_HEADER;
} else {
// Body start
bool thisLineHasEmote = false;
for (int e = 0; e < numEmotes; ++e) {
if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
thisLineHasEmote = true;
break;
int topY;
if (isHeader[b.start]) {
// Header start
constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2
topY = visualTop - BUBBLE_PAD_TOP_HEADER;
} else {
// Body start
bool thisLineHasEmote = false;
for (int e = 0; e < numEmotes; ++e) {
if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
thisLineHasEmote = true;
break;
}
}
if (thisLineHasEmote) {
constexpr int EMOTE_PADDING_ABOVE = 4;
visualTop -= EMOTE_PADDING_ABOVE;
}
topY = visualTop - BUBBLE_PAD_Y;
}
if (thisLineHasEmote) {
constexpr int EMOTE_PADDING_ABOVE = 4;
visualTop -= EMOTE_PADDING_ABOVE;
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
int bottomY = visualBottom + BUBBLE_PAD_Y;
if (bi + 1 < blocks.size()) {
int nextHeaderIndex = (int)blocks[bi + 1].start;
int nextTop = lineTop[nextHeaderIndex];
int maxBottom = nextTop - 1 - bubbleGapY;
if (bottomY > maxBottom)
bottomY = maxBottom;
}
topY = visualTop - BUBBLE_PAD_Y;
}
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
int bottomY = visualBottom + BUBBLE_PAD_Y;
if (bi + 1 < blocks.size()) {
int nextHeaderIndex = (int)blocks[bi + 1].start;
int nextTop = lineTop[nextHeaderIndex];
int maxBottom = nextTop - 1 - bubbleGapY;
if (bottomY > maxBottom)
bottomY = maxBottom;
}
if (bottomY <= topY + 2)
continue;
if (bottomY <= topY + 2)
continue;
if (bottomY < contentTop || topY > contentBottom - 1)
continue;
if (bottomY < contentTop || topY > contentBottom - 1)
continue;
int maxLineW = 0;
int maxLineW = 0;
for (size_t i = b.start; i <= b.end; ++i) {
int w = 0;
if (isHeader[i]) {
w = display->getStringWidth(cachedLines[i].c_str());
if (b.mine)
w += 12; // room for ACK/NACK/relay mark
} else {
w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
for (size_t i = b.start; i <= b.end; ++i) {
int w = 0;
if (isHeader[i]) {
w = display->getStringWidth(cachedLines[i].c_str());
if (b.mine)
w += 12; // room for ACK/NACK/relay mark
} else {
w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
}
if (w > maxLineW)
maxLineW = w;
}
if (w > maxLineW)
maxLineW = w;
}
int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (BUBBLE_PAD_X * 2));
int bubbleH = (bottomY - topY) + 1;
int bubbleX = 0;
if (b.mine) {
bubbleX = rightEdge - bubbleW;
} else {
bubbleX = x;
}
if (bubbleX < x)
bubbleX = x;
if (bubbleX + bubbleW > rightEdge)
bubbleW = std::max(1, rightEdge - bubbleX);
if (bubbleW > 1 && bubbleH > 1) {
int x1 = bubbleX + bubbleW - 1;
int y1 = topY + bubbleH - 1;
int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (textIndent * 2));
int bubbleH = (bottomY - topY) + 1;
int bubbleX = 0;
if (b.mine) {
// Send Message (Right side)
display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH);
// Top Right Corner
display->drawRect(x1, topY, 2, 1);
display->drawRect(x1, topY, 1, 2);
// Bottom Right Corner
display->drawRect(x1 - 1, bottomY - 2, 2, 1);
display->drawRect(x1, bottomY - 3, 1, 2);
// Knock the corners off to make a bubble
display->setColor(BLACK);
display->drawRect(x1 - bubbleW, topY - 1, 1, 1);
display->drawRect(x1 - bubbleW, bottomY - 1, 1, 1);
display->setColor(WHITE);
bubbleX = rightEdge - bubbleW;
} else {
// Received Message (Left Side)
display->drawRect(bubbleX, topY, bubbleW + 1, bubbleH);
// Top Left Corner
display->drawRect(bubbleX + 1, topY + 1, 2, 1);
display->drawRect(bubbleX + 1, topY + 1, 1, 2);
// Bottom Left Corner
display->drawRect(bubbleX + 1, bottomY - 1, 2, 1);
display->drawRect(bubbleX + 1, bottomY - 2, 1, 2);
// Knock the corners off to make a bubble
display->setColor(BLACK);
display->drawRect(bubbleX + bubbleW, topY, 1, 1);
display->drawRect(bubbleX + bubbleW, bottomY, 1, 1);
display->setColor(WHITE);
bubbleX = x;
}
if (bubbleX < x)
bubbleX = x;
if (bubbleX + bubbleW > rightEdge)
bubbleW = std::max(1, rightEdge - bubbleX);
// Draw rounded rectangle bubble
if (bubbleW > BUBBLE_RADIUS * 2 && bubbleH > BUBBLE_RADIUS * 2) {
const int r = BUBBLE_RADIUS;
const int bx = bubbleX;
const int by = topY;
const int bw = bubbleW;
const int bh = bubbleH;
// Draw the 4 corner arcs using drawCircleQuads
display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left
display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right
display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left
display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right
// Draw the 4 edges between corners
display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge
display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge
display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge
display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge
} else if (bubbleW > 1 && bubbleH > 1) {
// Fallback to simple rectangle for very small bubbles
display->drawRect(bubbleX, topY, bubbleW, bubbleH);
}
}
}
} // end if (showBubbles)
// Render visible lines
int lineY = yOffset;
@@ -916,11 +911,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int headerX;
if (isMine[i]) {
// push header left to avoid overlap with scrollbar
headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - BUBBLE_TEXT_INDENT;
headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - (showBubbles ? textIndent : 0);
if (headerX < LEFT_MARGIN)
headerX = LEFT_MARGIN;
} else {
headerX = x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT;
headerX = x + textIndent;
}
display->drawString(headerX, lineY, cachedLines[i].c_str());
@@ -960,14 +955,13 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
if (isMine[i]) {
// Calculate actual rendered width including emotes
int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - BUBBLE_TEXT_INDENT;
int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - (showBubbles ? textIndent : 0);
if (rightX < LEFT_MARGIN)
rightX = LEFT_MARGIN;
drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes);
} else {
drawStringWithEmotes(display, x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT, lineY, cachedLines[i], emotes,
numEmotes);
drawStringWithEmotes(display, x + textIndent, lineY, cachedLines[i], emotes, numEmotes);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,257 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./KeyboardApplet.h"
using namespace NicheGraphics;
InkHUD::KeyboardApplet::KeyboardApplet()
{
// Calculate row widths
for (uint8_t row = 0; row < KBD_ROWS; row++) {
rowWidths[row] = 0;
for (uint8_t col = 0; col < KBD_COLS; col++)
rowWidths[row] += keyWidths[row * KBD_COLS + col];
}
}
void InkHUD::KeyboardApplet::onRender(bool full)
{
uint16_t em = fontSmall.lineHeight(); // 16 pt
uint16_t keyH = Y(1.0) / KBD_ROWS;
int16_t keyTopPadding = (keyH - fontSmall.lineHeight()) / 2;
if (full) { // Draw full keyboard
for (uint8_t row = 0; row < KBD_ROWS; row++) {
// Calculate the remaining space to be used as padding
int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
// Draw keys
uint16_t xPos = 0;
for (uint8_t col = 0; col < KBD_COLS; col++) {
Color fgcolor = BLACK;
uint8_t index = row * KBD_COLS + col;
uint16_t keyX = ((xPos * em) >> 4) + ((col * keyXPadding) / (KBD_COLS - 1));
uint16_t keyY = row * keyH;
uint16_t keyW = (keyWidths[index] * em) >> 4;
if (index == selectedKey) {
fgcolor = WHITE;
fillRect(keyX, keyY, keyW, keyH, BLACK);
}
drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[index], fgcolor);
xPos += keyWidths[index];
}
}
} else { // Only draw the difference
if (selectedKey != prevSelectedKey) {
// Draw previously selected key
uint8_t row = prevSelectedKey / KBD_COLS;
int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
uint16_t xPos = 0;
for (uint8_t i = prevSelectedKey - (prevSelectedKey % KBD_COLS); i < prevSelectedKey; i++)
xPos += keyWidths[i];
uint16_t keyX = ((xPos * em) >> 4) + (((prevSelectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1));
uint16_t keyY = row * keyH;
uint16_t keyW = (keyWidths[prevSelectedKey] * em) >> 4;
fillRect(keyX, keyY, keyW, keyH, WHITE);
drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[prevSelectedKey], BLACK);
// Draw newly selected key
row = selectedKey / KBD_COLS;
keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
xPos = 0;
for (uint8_t i = selectedKey - (selectedKey % KBD_COLS); i < selectedKey; i++)
xPos += keyWidths[i];
keyX = ((xPos * em) >> 4) + (((selectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1));
keyY = row * keyH;
keyW = (keyWidths[selectedKey] * em) >> 4;
fillRect(keyX, keyY, keyW, keyH, BLACK);
drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[selectedKey], WHITE);
}
}
prevSelectedKey = selectedKey;
}
// Draw the key label corresponding to the char
// for most keys it draws the character itself
// for ['\b', '\n', ' ', '\x1b'] it draws special glyphs
void InkHUD::KeyboardApplet::drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color)
{
if (key == '\b') {
// Draw backspace glyph: 13 x 9 px
/**
* [][][][][][][][][]
* [][] []
* [][] [] [] []
* [][] [] [] []
* [][] [] []
* [][] [] [] []
* [][] [] [] []
* [][] []
* [][][][][][][][][]
*/
const uint8_t bsBitmap[] = {0x0f, 0xf8, 0x18, 0x08, 0x32, 0x28, 0x61, 0x48, 0xc0,
0x88, 0x61, 0x48, 0x32, 0x28, 0x18, 0x08, 0x0f, 0xf8};
uint16_t leftPadding = (width - 13) >> 1;
drawBitmap(left + leftPadding, top + 1, bsBitmap, 13, 9, color);
} else if (key == '\n') {
// Draw done glyph: 12 x 9 px
/**
* [][]
* [][]
* [][]
* [][]
* [][]
* [][] [][]
* [][] [][]
* [][][]
* []
*/
const uint8_t doneBitmap[] = {0x00, 0x30, 0x00, 0x60, 0x00, 0xc0, 0x01, 0x80, 0x03,
0x00, 0xc6, 0x00, 0x6c, 0x00, 0x38, 0x00, 0x10, 0x00};
uint16_t leftPadding = (width - 12) >> 1;
drawBitmap(left + leftPadding, top + 1, doneBitmap, 12, 9, color);
} else if (key == ' ') {
// Draw space glyph: 13 x 9 px
/**
*
*
*
*
* [] []
* [] []
* [][][][][][][][][][][][][]
*
*
*/
const uint8_t spaceBitmap[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
0x08, 0x80, 0x08, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00};
uint16_t leftPadding = (width - 13) >> 1;
drawBitmap(left + leftPadding, top + 1, spaceBitmap, 13, 9, color);
} else if (key == '\x1b') {
setTextColor(color);
std::string keyText = "ESC";
uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1;
printAt(left + leftPadding, top, keyText);
} else {
setTextColor(color);
if (key >= 0x61)
key -= 32; // capitalize
std::string keyText = std::string(1, key);
uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1;
printAt(left + leftPadding, top, keyText);
}
}
void InkHUD::KeyboardApplet::onForeground()
{
handleInput = true; // Intercept the button input for our applet
// Select the first key
selectedKey = 0;
prevSelectedKey = 0;
}
void InkHUD::KeyboardApplet::onBackground()
{
handleInput = false;
}
void InkHUD::KeyboardApplet::onButtonShortPress()
{
char key = keys[selectedKey];
if (key == '\n') {
inkhud->freeTextDone();
inkhud->closeKeyboard();
} else if (key == '\x1b') {
inkhud->freeTextCancel();
inkhud->closeKeyboard();
} else {
inkhud->freeText(key);
}
}
void InkHUD::KeyboardApplet::onButtonLongPress()
{
char key = keys[selectedKey];
if (key == '\n') {
inkhud->freeTextDone();
inkhud->closeKeyboard();
} else if (key == '\x1b') {
inkhud->freeTextCancel();
inkhud->closeKeyboard();
} else {
if (key >= 0x61)
key -= 32; // capitalize
inkhud->freeText(key);
}
}
void InkHUD::KeyboardApplet::onExitShort()
{
inkhud->freeTextCancel();
inkhud->closeKeyboard();
}
void InkHUD::KeyboardApplet::onExitLong()
{
inkhud->freeTextCancel();
inkhud->closeKeyboard();
}
void InkHUD::KeyboardApplet::onNavUp()
{
if (selectedKey < KBD_COLS) // wrap
selectedKey += KBD_COLS * (KBD_ROWS - 1);
else // move 1 row back
selectedKey -= KBD_COLS;
// Request rendering over the previously drawn render
requestUpdate(EInk::UpdateTypes::FAST, false);
// Force an update to bypass lockRequests
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
void InkHUD::KeyboardApplet::onNavDown()
{
selectedKey += KBD_COLS;
selectedKey %= (KBD_COLS * KBD_ROWS);
// Request rendering over the previously drawn render
requestUpdate(EInk::UpdateTypes::FAST, false);
// Force an update to bypass lockRequests
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
void InkHUD::KeyboardApplet::onNavLeft()
{
if (selectedKey % KBD_COLS == 0) // wrap
selectedKey += KBD_COLS - 1;
else // move 1 column back
selectedKey--;
// Request rendering over the previously drawn render
requestUpdate(EInk::UpdateTypes::FAST, false);
// Force an update to bypass lockRequests
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
void InkHUD::KeyboardApplet::onNavRight()
{
if (selectedKey % KBD_COLS == KBD_COLS - 1) // wrap
selectedKey -= KBD_COLS - 1;
else // move 1 column forward
selectedKey++;
// Request rendering over the previously drawn render
requestUpdate(EInk::UpdateTypes::FAST, false);
// Force an update to bypass lockRequests
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
uint16_t InkHUD::KeyboardApplet::getKeyboardHeight()
{
const uint16_t keyH = fontSmall.lineHeight() * 1.2;
return keyH * KBD_ROWS;
}
#endif

View File

@@ -0,0 +1,66 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
System Applet to render an on-screeen keyboard
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/InkHUD.h"
#include "graphics/niche/InkHUD/SystemApplet.h"
#include <string>
namespace NicheGraphics::InkHUD
{
class KeyboardApplet : public SystemApplet
{
public:
KeyboardApplet();
void onRender(bool full) override;
void onForeground() override;
void onBackground() override;
void onButtonShortPress() override;
void onButtonLongPress() override;
void onExitShort() override;
void onExitLong() override;
void onNavUp() override;
void onNavDown() override;
void onNavLeft() override;
void onNavRight() override;
static uint16_t getKeyboardHeight(); // used to set the keyboard tile height
private:
void drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color);
static const uint8_t KBD_COLS = 11;
static const uint8_t KBD_ROWS = 4;
const char keys[KBD_COLS * KBD_ROWS] = {
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b', // row 0
'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n', // row 1
'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '!', ' ', // row 2
'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '?', '\x1b' // row 3
};
// This array represents the widths of each key in points
// 16 pt = line height of the text
const uint16_t keyWidths[KBD_COLS * KBD_ROWS] = {
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 0
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 1
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 2
16, 16, 16, 16, 16, 16, 16, 10, 10, 12, 40 // row 3
};
uint16_t rowWidths[KBD_ROWS];
uint8_t selectedKey = 0; // selected key index
uint8_t prevSelectedKey = 0;
};
} // namespace NicheGraphics::InkHUD
#endif

View File

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

View File

@@ -21,7 +21,7 @@ class LogoApplet : public SystemApplet, public concurrency::OSThread
{
public:
LogoApplet();
void onRender() override;
void onRender(bool full) override;
void onForeground() override;
void onBackground() override;
void onShutdown() override;

View File

@@ -19,10 +19,10 @@ namespace NicheGraphics::InkHUD
enum MenuAction {
NO_ACTION,
SEND_PING,
FREE_TEXT,
STORE_CANNEDMESSAGE_SELECTION,
SEND_CANNEDMESSAGE,
SHUTDOWN,
BACK,
NEXT_TILE,
TOGGLE_BACKLIGHT,
TOGGLE_GPS,

View File

@@ -90,6 +90,8 @@ void InkHUD::MenuApplet::onForeground()
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
OSThread::enabled = true;
freeTextMode = false;
// Upgrade the refresh to FAST, for guaranteed responsiveness
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
@@ -116,6 +118,8 @@ void InkHUD::MenuApplet::onBackground()
SystemApplet::lockRequests = false;
SystemApplet::handleInput = false;
handleFreeText = false;
// Restore the user applet whose tile we borrowed
if (borrowedTileOwner)
borrowedTileOwner->bringToForeground();
@@ -325,10 +329,6 @@ void InkHUD::MenuApplet::execute(MenuItem item)
}
break;
case BACK:
showPage(item.nextPage);
return;
case NEXT_TILE:
inkhud->nextTile();
// Unselect menu item after tile change
@@ -344,12 +344,26 @@ void InkHUD::MenuApplet::execute(MenuItem item)
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL);
break;
case FREE_TEXT:
OSThread::enabled = false;
handleFreeText = true;
cm.freeTextItem.rawText.erase(); // clear the previous freetext message
freeTextMode = true; // render input field instead of normal menu
// Open the on-screen keyboard if the joystick is enabled
if (settings->joystick.enabled)
inkhud->openKeyboard();
break;
case STORE_CANNEDMESSAGE_SELECTION:
cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
if (!settings->joystick.enabled)
cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
else
cm.selectedMessageItem = &cm.messageItems.at(cursor - 2); // Minus two: offset for the "Send Ping" and free text entry
break;
case SEND_CANNEDMESSAGE:
cm.selectedRecipientItem = &cm.recipientItems.at(cursor);
// send selected message
sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str());
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here
break;
@@ -868,6 +882,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
switch (page) {
case ROOT:
previousPage = MenuPage::EXIT;
// Optional: next applet
if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1)
items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown
@@ -878,7 +893,6 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
items.push_back(MenuItem("Node Config", MenuPage::NODE_CONFIG));
items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
previousPage = MenuPage::EXIT;
break;
case SEND:
@@ -888,11 +902,12 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
case CANNEDMESSAGE_RECIPIENT:
populateRecipientPage();
previousPage = MenuPage::OPTIONS;
previousPage = MenuPage::SEND;
break;
case OPTIONS:
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::ROOT));
previousPage = MenuPage::ROOT;
items.push_back(MenuItem("Back", previousPage));
// Optional: backlight
if (settings->optionalMenuItems.backlight)
items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label
@@ -916,31 +931,32 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
invertedColors = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
items.push_back(MenuItem("Invert Color", MenuAction::TOGGLE_INVERT_COLOR, MenuPage::OPTIONS, &invertedColors));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
previousPage = MenuPage::ROOT;
break;
case APPLETS:
populateAppletPage(); // must be first
items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
previousPage = MenuPage::OPTIONS;
populateAppletPage(); // must be first
items.insert(items.begin(), MenuItem("Back", previousPage));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
case AUTOSHOW:
populateAutoshowPage(); // must be first
items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
previousPage = MenuPage::OPTIONS;
populateAutoshowPage(); // must be first
items.insert(items.begin(), MenuItem("Back", previousPage));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
case RECENTS:
previousPage = MenuPage::OPTIONS;
populateRecentsPage(); // builds only the options
items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS));
items.insert(items.begin(), MenuItem("Back", previousPage));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
case NODE_CONFIG:
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::ROOT));
previousPage = MenuPage::ROOT;
items.push_back(MenuItem("Back", previousPage));
// Radio Config Section
items.push_back(MenuItem::Header("Radio Config"));
items.push_back(MenuItem("LoRa", MenuPage::NODE_CONFIG_LORA));
@@ -965,8 +981,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
break;
case NODE_CONFIG_DEVICE: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
const char *role = DisplayFormatters::getDeviceRole(config.device.role);
nodeConfigLabels.emplace_back("Role: " + std::string(role));
@@ -981,7 +997,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_POSITION: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
#if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS
const auto mode = config.position.gps_mode;
if (mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) {
@@ -996,7 +1013,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_POWER: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
#if defined(ARCH_ESP32)
items.push_back(MenuItem("Powersave", MenuAction::TOGGLE_POWER_SAVE, MenuPage::EXIT, &config.power.is_power_saving));
#endif
@@ -1029,7 +1047,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_POWER_ADC_CAL: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_POWER));
previousPage = MenuPage::NODE_CONFIG_POWER;
items.push_back(MenuItem("Back", previousPage));
// Instruction text (header-style, non-selectable)
items.push_back(MenuItem::Header("Run on full charge Only"));
@@ -1042,7 +1061,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_NETWORK: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
const char *wifiLabel = config.network.wifi_enabled ? "WiFi: On" : "WiFi: Off";
@@ -1099,7 +1119,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_DISPLAY: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::NODE_CONFIG_DISPLAY,
&config.display.use_12h_clock));
@@ -1114,7 +1135,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_BLUETOOTH: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
const char *btLabel = config.bluetooth.enabled ? "Bluetooth: On" : "Bluetooth: Off";
items.push_back(MenuItem(btLabel, MenuAction::TOGGLE_BLUETOOTH, MenuPage::EXIT));
@@ -1127,8 +1149,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_LORA: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
const char *region = myRegion ? myRegion->name : "Unset";
nodeConfigLabels.emplace_back("Region: " + std::string(region));
@@ -1150,7 +1172,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_CHANNELS: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
meshtastic_Channel &ch = channels.getByIndex(i);
@@ -1181,7 +1204,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_CHANNEL_DETAIL: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_CHANNELS));
previousPage = MenuPage::NODE_CONFIG_CHANNELS;
items.push_back(MenuItem("Back", previousPage));
meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
@@ -1226,7 +1250,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_CHANNEL_PRECISION: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_CHANNEL_DETAIL));
previousPage = MenuPage::NODE_CONFIG_CHANNEL_DETAIL;
items.push_back(MenuItem("Back", previousPage));
meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
if (!ch.settings.has_module_settings || ch.settings.module_settings.position_precision == 0) {
items.push_back(MenuItem("Position is Off", MenuPage::NODE_CONFIG_CHANNEL_DETAIL));
@@ -1247,7 +1272,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_DEVICE_ROLE: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_DEVICE));
previousPage = MenuPage::NODE_CONFIG_DEVICE;
items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("Client", MenuAction::SET_ROLE_CLIENT, MenuPage::EXIT));
items.push_back(MenuItem("Client Mute", MenuAction::SET_ROLE_CLIENT_MUTE, MenuPage::EXIT));
items.push_back(MenuItem("Router", MenuAction::SET_ROLE_ROUTER, MenuPage::EXIT));
@@ -1257,7 +1283,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case TIMEZONE:
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_DEVICE));
previousPage = MenuPage::NODE_CONFIG_DEVICE;
items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("US/Hawaii", SET_TZ_US_HAWAII, MenuPage::NODE_CONFIG_DEVICE));
items.push_back(MenuItem("US/Alaska", SET_TZ_US_ALASKA, MenuPage::NODE_CONFIG_DEVICE));
items.push_back(MenuItem("US/Pacific", SET_TZ_US_PACIFIC, MenuPage::NODE_CONFIG_DEVICE));
@@ -1279,7 +1306,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
break;
case REGION:
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_LORA));
previousPage = MenuPage::NODE_CONFIG_LORA;
items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("US", MenuAction::SET_REGION_US, MenuPage::EXIT));
items.push_back(MenuItem("EU 868", MenuAction::SET_REGION_EU_868, MenuPage::EXIT));
items.push_back(MenuItem("EU 433", MenuAction::SET_REGION_EU_433, MenuPage::EXIT));
@@ -1310,7 +1338,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
break;
case NODE_CONFIG_PRESET: {
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_LORA));
previousPage = MenuPage::NODE_CONFIG_LORA;
items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("Long Moderate", MenuAction::SET_PRESET_LONG_MODERATE, MenuPage::EXIT));
items.push_back(MenuItem("Long Fast", MenuAction::SET_PRESET_LONG_FAST, MenuPage::EXIT));
items.push_back(MenuItem("Medium Slow", MenuAction::SET_PRESET_MEDIUM_SLOW, MenuPage::EXIT));
@@ -1323,7 +1352,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
// Administration Section
case NODE_CONFIG_ADMIN_RESET:
items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
previousPage = MenuPage::NODE_CONFIG;
items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("Reset All", MenuAction::RESET_NODEDB_ALL, MenuPage::EXIT));
items.push_back(MenuItem("Keep Favorites Only", MenuAction::RESET_NODEDB_KEEP_FAVORITES, MenuPage::EXIT));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
@@ -1361,8 +1391,14 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
currentPage = page;
}
void InkHUD::MenuApplet::onRender()
void InkHUD::MenuApplet::onRender(bool full)
{
// Free text mode draws a text input field and skips the normal rendering
if (freeTextMode) {
drawInputField(0, fontSmall.lineHeight(), X(1.0), Y(1.0) - fontSmall.lineHeight() - 1, cm.freeTextItem.rawText);
return;
}
if (items.size() == 0)
LOG_ERROR("Empty Menu");
@@ -1481,44 +1517,48 @@ void InkHUD::MenuApplet::onRender()
void InkHUD::MenuApplet::onButtonShortPress()
{
// Push the auto-close timer back
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!freeTextMode) {
// Push the auto-close timer back
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!settings->joystick.enabled) {
if (!cursorShown) {
cursorShown = true;
cursor = 0;
} else {
do {
cursor = (cursor + 1) % items.size();
} while (items.at(cursor).isHeader);
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
} else {
if (cursorShown)
execute(items.at(cursor));
else
showPage(MenuPage::EXIT);
if (!wantsToRender())
if (!settings->joystick.enabled) {
if (!cursorShown) {
cursorShown = true;
cursor = 0;
} else {
do {
cursor = (cursor + 1) % items.size();
} while (items.at(cursor).isHeader);
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
} else {
if (cursorShown)
execute(items.at(cursor));
else
showPage(MenuPage::EXIT);
if (!wantsToRender())
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
}
}
void InkHUD::MenuApplet::onButtonLongPress()
{
// Push the auto-close timer back
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!freeTextMode) {
// Push the auto-close timer back
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (cursorShown)
execute(items.at(cursor));
else
showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close
if (cursorShown)
execute(items.at(cursor));
else
showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close
// If we didn't already request a specialized update, when handling a menu action,
// then perform the usual fast update.
// FAST keeps things responsive: important because we're dealing with user input
if (!wantsToRender())
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
// If we didn't already request a specialized update, when handling a menu action,
// then perform the usual fast update.
// FAST keeps things responsive: important because we're dealing with user input
if (!wantsToRender())
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
}
void InkHUD::MenuApplet::onExitShort()
@@ -1531,56 +1571,107 @@ void InkHUD::MenuApplet::onExitShort()
void InkHUD::MenuApplet::onNavUp()
{
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!freeTextMode) {
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!cursorShown) {
cursorShown = true;
cursor = 0;
} else {
do {
if (cursor == 0)
cursor = items.size() - 1;
else
cursor--;
} while (items.at(cursor).isHeader);
if (!cursorShown) {
cursorShown = true;
cursor = 0;
} else {
do {
if (cursor == 0)
cursor = items.size() - 1;
else
cursor--;
} while (items.at(cursor).isHeader);
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onNavDown()
{
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!freeTextMode) {
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!cursorShown) {
cursorShown = true;
cursor = 0;
} else {
do {
cursor = (cursor + 1) % items.size();
} while (items.at(cursor).isHeader);
if (!cursorShown) {
cursorShown = true;
cursor = 0;
} else {
do {
cursor = (cursor + 1) % items.size();
} while (items.at(cursor).isHeader);
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onNavLeft()
{
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!freeTextMode) {
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
// Go to the previous menu page
showPage(previousPage);
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
// Go to the previous menu page
showPage(previousPage);
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
}
void InkHUD::MenuApplet::onNavRight()
{
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (!freeTextMode) {
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
if (cursorShown)
execute(items.at(cursor));
if (!wantsToRender())
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
}
if (cursorShown)
execute(items.at(cursor));
if (!wantsToRender())
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
void InkHUD::MenuApplet::onFreeText(char c)
{
if (cm.freeTextItem.rawText.length() >= menuTextLimit && c != '\b')
return;
if (c == '\b') {
if (!cm.freeTextItem.rawText.empty())
cm.freeTextItem.rawText.pop_back();
} else {
cm.freeTextItem.rawText += c;
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onFreeTextDone()
{
// Restart the auto-close timeout
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
OSThread::enabled = true;
handleFreeText = false;
freeTextMode = false;
if (!cm.freeTextItem.rawText.empty()) {
cm.selectedMessageItem = &cm.freeTextItem;
showPage(MenuPage::CANNEDMESSAGE_RECIPIENT);
}
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onFreeTextCancel()
{
// Restart the auto-close timeout
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
OSThread::enabled = true;
handleFreeText = false;
freeTextMode = false;
// Clear the free text message
cm.freeTextItem.rawText.erase();
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
// Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu
@@ -1635,6 +1726,10 @@ void InkHUD::MenuApplet::populateSendPage()
// Position / NodeInfo packet
items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT));
// If joystick is available, include the Free Text option
if (settings->joystick.enabled)
items.push_back(MenuItem("Free Text", MenuAction::FREE_TEXT, MenuPage::SEND));
// One menu item for each canned message
uint8_t count = cm.store->size();
for (uint8_t i = 0; i < count; i++) {
@@ -1734,6 +1829,48 @@ void InkHUD::MenuApplet::populateRecipientPage()
items.push_back(MenuItem("Exit", MenuPage::EXIT));
}
void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, std::string text)
{
setFont(fontSmall);
uint16_t wrapMaxH = 0;
// Draw the text, input box, and cursor
// Adjusting the box for screen height
while (wrapMaxH < height - fontSmall.lineHeight()) {
wrapMaxH += fontSmall.lineHeight();
}
// If the text is so long that it goes outside of the input box, the text is actually rendered off screen.
uint32_t textHeight = getWrappedTextHeight(0, width - 5, text);
if (!text.empty()) {
uint16_t textPadding = X(1.0) > Y(1.0) ? wrapMaxH - textHeight : wrapMaxH - textHeight + 1;
if (textHeight > wrapMaxH)
printWrapped(2, textPadding, width - 5, text);
else
printWrapped(2, top + 2, width - 5, text);
}
uint16_t textCursorX = text.empty() ? 1 : getCursorX();
uint16_t textCursorY = text.empty() ? fontSmall.lineHeight() + 2 : getCursorY() - fontSmall.lineHeight() + 3;
if (textCursorX + 1 > width - 5) {
textCursorX = getCursorX() - width + 5;
textCursorY += fontSmall.lineHeight();
}
fillRect(textCursorX + 1, textCursorY, 1, fontSmall.lineHeight(), BLACK);
// A white rectangle clears the top part of the screen for any text that's printed beyond the input box
fillRect(0, 0, X(1.0), top, WHITE);
// Draw character limit
std::string ftlen = std::to_string(text.length()) + "/" + to_string(menuTextLimit);
uint16_t textLen = getTextWidth(ftlen);
printAt(X(1.0) - textLen - 2, 0, ftlen);
// Draw the border
drawRect(0, top, width, wrapMaxH + 5, BLACK);
}
// Renders the panel shown at the top of the root menu.
// Displays the clock, and several other pieces of instantaneous system info,
// which we'd prefer not to have displayed in a normal applet, as they update too frequently.
@@ -1875,4 +2012,4 @@ void InkHUD::MenuApplet::freeCannedMessageResources()
cm.messageItems.clear();
cm.recipientItems.clear();
}
#endif // MESHTASTIC_INCLUDE_INKHUD
#endif // MESHTASTIC_INCLUDE_INKHUD

View File

@@ -32,7 +32,10 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
void onNavDown() override;
void onNavLeft() override;
void onNavRight() override;
void onRender() override;
void onFreeText(char c) override;
void onFreeTextDone() override;
void onFreeTextCancel() override;
void onRender(bool full) override;
void show(Tile *t); // Open the menu, onto a user tile
void setStartPage(MenuPage page);
@@ -51,6 +54,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height,
std::string text); // Draw input field for free text
uint16_t getSystemInfoPanelHeight();
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
uint16_t *height = nullptr); // Info panel at top of root menu
@@ -62,8 +67,9 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
MenuPage previousPage = MenuPage::EXIT;
uint8_t cursor = 0; // Which menu item is currently highlighted
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
bool freeTextMode = false;
uint16_t systemInfoPanelHeight = 0; // Need to know before we render
uint16_t menuTextLimit = 200;
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
std::vector<std::string> nodeConfigLabels; // Persistent labels for Node Config pages
@@ -104,6 +110,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
// Cleared onBackground (when MenuApplet closes)
std::vector<MessageItem> messageItems;
std::vector<RecipientItem> recipientItems;
MessageItem freeTextItem;
} cm;
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ InkHUD::TipsApplet::TipsApplet()
bringToForeground();
}
void InkHUD::TipsApplet::onRender()
void InkHUD::TipsApplet::onRender(bool full)
{
switch (tipQueue.front()) {
case Tip::WELCOME:
@@ -261,7 +261,7 @@ void InkHUD::TipsApplet::onBackground()
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
}
// While our SystemApplet::handleInput flag is true
@@ -292,9 +292,8 @@ void InkHUD::TipsApplet::onButtonShortPress()
inkhud->persistence->saveSettings();
}
// Close applet and clean the screen
// Close applet
sendToBackground();
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
} else {
requestUpdate();
}
@@ -306,4 +305,4 @@ void InkHUD::TipsApplet::onExitShort()
onButtonShortPress();
}
#endif
#endif

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -238,6 +238,39 @@ void InkHUD::Events::onNavRight()
}
}
void InkHUD::Events::onFreeText(char c)
{
// Trigger the first system applet that wants to handle the new character
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleFreeText) {
sa->onFreeText(c);
break;
}
}
}
void InkHUD::Events::onFreeTextDone()
{
// Trigger the first system applet that wants to handle it
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleFreeText) {
sa->onFreeTextDone();
break;
}
}
}
void InkHUD::Events::onFreeTextCancel()
{
// Trigger the first system applet that wants to handle it
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleFreeText) {
sa->onFreeTextCancel();
break;
}
}
}
// Callback for deepSleepObserver
// Returns 0 to signal that we agree to sleep now
int InkHUD::Events::beforeDeepSleep(void *unused)
@@ -266,7 +299,7 @@ int InkHUD::Events::beforeDeepSleep(void *unused)
// then prepared a final powered-off screen for us, which shows device shortname.
// We're updating to show that one now.
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false);
delay(1000); // Cooldown, before potentially yanking display power
// InkHUD shutdown complete

View File

@@ -37,6 +37,11 @@ class Events
void onNavLeft(); // Navigate left
void onNavRight(); // Navigate right
// Free text typing events
void onFreeText(char c); // New freetext character input
void onFreeTextDone();
void onFreeTextCancel();
int beforeDeepSleep(void *unused); // Prepare for shutdown
int beforeReboot(void *unused); // Prepare for reboot
int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message

View File

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

View File

@@ -63,6 +63,11 @@ class InkHUD
void navLeft();
void navRight();
// Freetext handlers
void freeText(char c);
void freeTextDone();
void freeTextCancel();
// Trigger UI changes
// - called by various InkHUD components
// - suitable(?) for use by aux button, connected in variant nicheGraphics.h
@@ -71,6 +76,8 @@ class InkHUD
void prevApplet();
void openMenu();
void openAlignStick();
void openKeyboard();
void closeKeyboard();
void nextTile();
void prevTile();
void rotate();
@@ -84,7 +91,8 @@ class InkHUD
// - called by various InkHUD components
void requestUpdate();
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool async = true);
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool all = false,
bool async = true);
void awaitUpdate();
// (Re)configuring WindowManager

View File

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

View File

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

View File

@@ -22,9 +22,11 @@ class SystemApplet : public Applet
public:
// System applets have the right to:
bool handleInput = false; // - respond to input from the user button
bool lockRendering = false; // - prevent other applets from being rendered during an update
bool lockRequests = false; // - prevent other applets from triggering display updates
bool handleInput = false; // - respond to input from the user button
bool handleFreeText = false; // - respond to free text input
bool lockRendering = false; // - prevent other applets from being rendered during an update
bool lockRequests = false; // - prevent other applets from triggering display updates
bool alwaysRender = false; // - render every time the screen is updated
virtual void onReboot() { onShutdown(); } // - handle reboot specially
virtual void onApplyingChanges() {}
@@ -41,4 +43,4 @@ class SystemApplet : public Applet
}; // namespace NicheGraphics::InkHUD
#endif
#endif

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -354,9 +354,9 @@ void setup()
digitalWrite(LED_POWER, LED_STATE_ON);
#endif
#ifdef USER_LED
pinMode(USER_LED, OUTPUT);
digitalWrite(USER_LED, HIGH ^ LED_STATE_ON);
#ifdef LED_NOTIFICATION
pinMode(LED_NOTIFICATION, OUTPUT);
digitalWrite(LED_NOTIFICATION, HIGH ^ LED_STATE_ON);
#endif
#ifdef WIFI_LED
@@ -713,7 +713,6 @@ void setup()
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X);
#endif
#ifdef HAS_SDCARD
@@ -929,6 +928,13 @@ void setup()
service = new MeshService();
service->init();
// Set osk_found for trackball/encoder devices BEFORE setupModules so CannedMessageModule can detect it
#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2)
#ifndef HAS_PHYSICAL_KEYBOARD
osk_found = true;
#endif
#endif
// Now that the mesh service is created, create any modules
setupModules();
@@ -1019,12 +1025,6 @@ void setup()
#endif
#endif
#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2)
#ifndef HAS_PHYSICAL_KEYBOARD
osk_found = true;
#endif
#endif
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER
// Start web server thread.
webServerThread = new WebServerThread();

View File

@@ -170,7 +170,7 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
err = lora.setCodingRate(cr);
err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it
if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);

View File

@@ -574,6 +574,10 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
#endif
#if defined(TFT_WIDTH) && defined(TFT_HEIGHT) && (TFT_WIDTH >= 200 || TFT_HEIGHT >= 200)
config.display.enable_message_bubbles = true;
#endif
#ifdef USERPREFS_CONFIG_DEVICE_ROLE
// Restrict ROUTER*, LOST AND FOUND roles for security reasons
if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER,
@@ -824,16 +828,10 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.output_ms = 500;
moduleConfig.external_notification.nag_timeout = 2;
#endif
#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || \
defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M6)
// Default to PIN_LED2 for external notification output (LED color depends on device variant)
#if defined(LED_NOTIFICATION)
moduleConfig.external_notification.enabled = true;
moduleConfig.external_notification.output = PIN_LED2;
#if defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3)
moduleConfig.external_notification.active = false;
#else
moduleConfig.external_notification.active = true;
#endif
moduleConfig.external_notification.output = LED_NOTIFICATION;
moduleConfig.external_notification.active = LED_STATE_ON;
moduleConfig.external_notification.alert_message = true;
moduleConfig.external_notification.output_ms = 1000;
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
@@ -857,15 +855,6 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.output_ms = 100;
moduleConfig.external_notification.active = true;
#endif
#ifdef ELECROW_ThinkNode_M1
// Default to Elecrow USER_LED (blue)
moduleConfig.external_notification.enabled = true;
moduleConfig.external_notification.output = USER_LED;
moduleConfig.external_notification.active = true;
moduleConfig.external_notification.alert_message = true;
moduleConfig.external_notification.output_ms = 1000;
moduleConfig.external_notification.nag_timeout = 60;
#endif
#ifdef T_LORA_PAGER
moduleConfig.canned_message.updown1_enabled = true;
moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A;
@@ -2228,8 +2217,8 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location,
} else if (location == meshtastic_AdminMessage_BackupLocation_SD) {
// TODO: After more mainline SD card support
}
return success;
#endif
return success;
}
/// Record an error that should be reported via analytics

View File

@@ -90,9 +90,9 @@ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpd
bool seenRecently = (found != NULL); // If found -> the packet was seen recently
// Check for hop_limit upgrade scenario
if (seenRecently && wasUpgraded && found->hop_limit < p->hop_limit) {
LOG_DEBUG("Packet History - Hop limit upgrade: packet 0x%08x from hop_limit=%d to hop_limit=%d", p->id, found->hop_limit,
p->hop_limit);
if (seenRecently && wasUpgraded && getHighestHopLimit(*found) < p->hop_limit) {
LOG_DEBUG("Packet History - Hop limit upgrade: packet 0x%08x from hop_limit=%d to hop_limit=%d", p->id,
getHighestHopLimit(*found), p->hop_limit);
*wasUpgraded = true;
} else if (wasUpgraded) {
*wasUpgraded = false; // Initialize to false if not an upgrade

View File

@@ -27,7 +27,7 @@
#include "platform/portduino/USBHal.h"
#endif
#ifdef ARCH_STM32WL>
#ifdef ARCH_STM32WL
#include "STM32WLE5JCInterface.h"
#endif

View File

@@ -620,15 +620,19 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
!(p->pki_encrypted != true && (strcasecmp(channels.getName(chIndex), Channels::serialChannel) == 0 ||
strcasecmp(channels.getName(chIndex), Channels::gpioChannel) == 0)) &&
// Check for valid keys and single node destination
config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr &&
// Check for a known public key for the destination
(node->user.public_key.size == 32) &&
config.security.private_key.size == 32 && !isBroadcast(p->to) &&
// Some portnums either make no sense to send with PKC
p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP &&
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) {
LOG_DEBUG("Use PKI!");
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
return meshtastic_Routing_Error_TOO_LARGE;
// Check for a known public key for the destination
if (node == nullptr || node->user.public_key.size != 32) {
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
p->decoded.portnum);
return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY;
}
if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) &&
memcmp(p->public_key.bytes, node->user.public_key.bytes, 32) != 0) {
LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes,

View File

@@ -126,7 +126,7 @@ template <typename T> bool SX128xInterface<T>::reconfigure()
if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
err = lora.setCodingRate(cr);
err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it
if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);

View File

@@ -505,6 +505,8 @@ typedef struct _meshtastic_Config_DisplayConfig {
/* If false (default), the device will use short names for various display screens.
If true, node names will show in long format */
bool use_long_node_name;
/* If true, the device will display message bubbles on screen. */
bool enable_message_bubbles;
} meshtastic_Config_DisplayConfig;
/* Lora Config */
@@ -732,7 +734,7 @@ extern "C" {
#define meshtastic_Config_PowerConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0, 0}
#define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0}
#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0}
#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0}
#define meshtastic_Config_BluetoothConfig_init_default {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
#define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
@@ -743,7 +745,7 @@ extern "C" {
#define meshtastic_Config_PowerConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0, 0}
#define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0}
#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0}
#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0}
#define meshtastic_Config_BluetoothConfig_init_zero {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
#define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
@@ -811,6 +813,7 @@ extern "C" {
#define meshtastic_Config_DisplayConfig_compass_orientation_tag 11
#define meshtastic_Config_DisplayConfig_use_12h_clock_tag 12
#define meshtastic_Config_DisplayConfig_use_long_node_name_tag 13
#define meshtastic_Config_DisplayConfig_enable_message_bubbles_tag 14
#define meshtastic_Config_LoRaConfig_use_preset_tag 1
#define meshtastic_Config_LoRaConfig_modem_preset_tag 2
#define meshtastic_Config_LoRaConfig_bandwidth_tag 3
@@ -957,7 +960,8 @@ X(a, STATIC, SINGULAR, BOOL, heading_bold, 9) \
X(a, STATIC, SINGULAR, BOOL, wake_on_tap_or_motion, 10) \
X(a, STATIC, SINGULAR, UENUM, compass_orientation, 11) \
X(a, STATIC, SINGULAR, BOOL, use_12h_clock, 12) \
X(a, STATIC, SINGULAR, BOOL, use_long_node_name, 13)
X(a, STATIC, SINGULAR, BOOL, use_long_node_name, 13) \
X(a, STATIC, SINGULAR, BOOL, enable_message_bubbles, 14)
#define meshtastic_Config_DisplayConfig_CALLBACK NULL
#define meshtastic_Config_DisplayConfig_DEFAULT NULL
@@ -1035,7 +1039,7 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg;
#define MESHTASTIC_MESHTASTIC_CONFIG_PB_H_MAX_SIZE meshtastic_Config_size
#define meshtastic_Config_BluetoothConfig_size 10
#define meshtastic_Config_DeviceConfig_size 100
#define meshtastic_Config_DisplayConfig_size 34
#define meshtastic_Config_DisplayConfig_size 36
#define meshtastic_Config_LoRaConfig_size 85
#define meshtastic_Config_NetworkConfig_IpV4Config_size 20
#define meshtastic_Config_NetworkConfig_size 204

View File

@@ -361,7 +361,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
/* Maximum encoded size of messages (where known) */
/* meshtastic_NodeDatabase_size depends on runtime parameters */
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
#define meshtastic_BackupPreferences_size 2362
#define meshtastic_BackupPreferences_size 2364
#define meshtastic_ChannelFile_size 718
#define meshtastic_DeviceState_size 1737
#define meshtastic_NodeInfoLite_size 196

View File

@@ -193,7 +193,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg;
/* Maximum encoded size of messages (where known) */
#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size
#define meshtastic_LocalConfig_size 749
#define meshtastic_LocalConfig_size 751
#define meshtastic_LocalModuleConfig_size 758
#ifdef __cplusplus

View File

@@ -22,10 +22,14 @@
class UdpMulticastHandler final
{
public:
UdpMulticastHandler() { udpIpAddress = IPAddress(224, 0, 0, 69); }
UdpMulticastHandler() : isRunning(false) { udpIpAddress = IPAddress(224, 0, 0, 69); }
void start()
{
if (isRunning) {
LOG_DEBUG("UDP multicast already running");
return;
}
if (udp.listenMulticast(udpIpAddress, UDP_MULTICAST_DEFAUL_PORT, 64)) {
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO)
LOG_DEBUG("UDP Listening on IP: %u.%u.%u.%u:%u", udpIpAddress[0], udpIpAddress[1], udpIpAddress[2], udpIpAddress[3],
@@ -34,13 +38,29 @@ class UdpMulticastHandler final
LOG_DEBUG("UDP Listening on IP: %s", WiFi.localIP().toString().c_str());
#endif
udp.onPacket([this](AsyncUDPPacket packet) { onReceive(packet); });
isRunning = true;
} else {
LOG_DEBUG("Failed to listen on UDP");
}
}
void stop()
{
if (!isRunning) {
return;
}
LOG_DEBUG("Stopping UDP multicast");
#if defined(ARCH_ESP32) || defined(ARCH_NRF52)
udp.close();
#endif
isRunning = false;
}
void onReceive(AsyncUDPPacket packet)
{
if (!isRunning) {
return;
}
size_t packetLength = packet.length();
#if defined(ARCH_NRF52)
IPAddress ip = packet.remoteIP();
@@ -67,7 +87,7 @@ class UdpMulticastHandler final
bool onSend(const meshtastic_MeshPacket *mp)
{
if (!mp || !udp) {
if (!isRunning || !mp || !udp) {
return false;
}
#if defined(ARCH_NRF52)
@@ -92,5 +112,6 @@ class UdpMulticastHandler final
private:
IPAddress udpIpAddress;
AsyncUDP udp;
bool isRunning;
};
#endif // HAS_UDP_MULTICAST

View File

@@ -391,6 +391,11 @@ static void WiFiEvent(WiFiEvent_t event)
LOG_INFO("Disconnected from WiFi access point");
#ifdef WIFI_LED
digitalWrite(WIFI_LED, LOW);
#endif
#if HAS_UDP_MULTICAST
if (udpHandler) {
udpHandler->stop();
}
#endif
if (!isReconnecting) {
WiFi.disconnect(false, true);
@@ -417,6 +422,11 @@ static void WiFiEvent(WiFiEvent_t event)
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
LOG_INFO("Lost IP address and IP address is reset to 0");
#if HAS_UDP_MULTICAST
if (udpHandler) {
udpHandler->stop();
}
#endif
if (!isReconnecting) {
WiFi.disconnect(false, true);
syslog.disable();

View File

@@ -106,4 +106,15 @@ const std::string vformat(const char *const zcFormat, ...)
std::vsnprintf(zc.data(), zc.size(), zcFormat, vaArgs);
va_end(vaArgs);
return std::string(zc.data(), iLen);
}
size_t pb_string_length(const char *str, size_t max_len)
{
size_t len = 0;
for (size_t i = 0; i < max_len; i++) {
if (str[i] != '\0') {
len = i + 1;
}
}
return len;
}

View File

@@ -35,4 +35,7 @@ bool isOneOf(int item, int count, ...);
const std::string vformat(const char *const zcFormat, ...);
// Get actual string length for nanopb char array fields.
size_t pb_string_length(const char *str, size_t max_len);
#define IS_ONE_OF(item, ...) isOneOf(item, sizeof((int[]){__VA_ARGS__}) / sizeof(int), __VA_ARGS__)

View File

@@ -6,6 +6,7 @@
#include "configuration.h"
#include "main.h"
#include "mesh/compression/unishox2.h"
#include "meshUtils.h"
#include "meshtastic/atak.pb.h"
AtakPluginModule *atakPluginModule;
@@ -70,16 +71,17 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
auto compressed = cloneTAKPacketData(t);
compressed.is_compressed = true;
if (t->has_contact) {
auto length = unishox2_compress_lines(t->contact.callsign, strlen(t->contact.callsign), compressed.contact.callsign,
sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
auto length = unishox2_compress_lines(
t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)),
compressed.contact.callsign, sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow contact.callsign. Revert to uncompressed packet");
return;
}
LOG_DEBUG("Compressed callsign: %d bytes", length);
length = unishox2_compress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign),
compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1,
USX_PSET_DFLT, NULL);
length = unishox2_compress_lines(
t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)),
compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow contact.device_callsign. Revert to uncompressed packet");
return;
@@ -87,9 +89,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
LOG_DEBUG("Compressed device_callsign: %d bytes", length);
}
if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) {
auto length = unishox2_compress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message),
compressed.payload_variant.chat.message,
sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL);
auto length = unishox2_compress_lines(
t->payload_variant.chat.message,
pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)),
compressed.payload_variant.chat.message, sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT,
NULL);
if (length < 0) {
LOG_WARN("Compress overflow chat.message. Revert to uncompressed packet");
return;
@@ -98,9 +102,9 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
if (t->payload_variant.chat.has_to) {
compressed.payload_variant.chat.has_to = true;
length = unishox2_compress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to),
compressed.payload_variant.chat.to,
sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
length = unishox2_compress_lines(
t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)),
compressed.payload_variant.chat.to, sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow chat.to. Revert to uncompressed packet");
return;
@@ -110,9 +114,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
if (t->payload_variant.chat.has_to_callsign) {
compressed.payload_variant.chat.has_to_callsign = true;
length = unishox2_compress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign),
compressed.payload_variant.chat.to_callsign,
sizeof(compressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL);
length = unishox2_compress_lines(
t->payload_variant.chat.to_callsign,
pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)),
compressed.payload_variant.chat.to_callsign, sizeof(compressed.payload_variant.chat.to_callsign) - 1,
USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow chat.to_callsign. Revert to uncompressed packet");
return;
@@ -134,18 +140,18 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
auto uncompressed = cloneTAKPacketData(t);
uncompressed.is_compressed = false;
if (t->has_contact) {
auto length =
unishox2_decompress_lines(t->contact.callsign, strlen(t->contact.callsign), uncompressed.contact.callsign,
sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
auto length = unishox2_decompress_lines(
t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)),
uncompressed.contact.callsign, sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow contact.callsign. Bailing out");
return;
}
LOG_DEBUG("Decompressed callsign: %d bytes", length);
length = unishox2_decompress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign),
uncompressed.contact.device_callsign,
sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
length = unishox2_decompress_lines(
t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)),
uncompressed.contact.device_callsign, sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow contact.device_callsign. Bailing out");
return;
@@ -153,9 +159,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
LOG_DEBUG("Decompressed device_callsign: %d bytes", length);
}
if (uncompressed.which_payload_variant == meshtastic_TAKPacket_chat_tag) {
auto length = unishox2_decompress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message),
uncompressed.payload_variant.chat.message,
sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL);
auto length = unishox2_decompress_lines(
t->payload_variant.chat.message,
pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)),
uncompressed.payload_variant.chat.message, sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT,
NULL);
if (length < 0) {
LOG_WARN("Decompress overflow chat.message. Bailing out");
return;
@@ -164,9 +172,9 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
if (t->payload_variant.chat.has_to) {
uncompressed.payload_variant.chat.has_to = true;
length = unishox2_decompress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to),
uncompressed.payload_variant.chat.to,
sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
length = unishox2_decompress_lines(
t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)),
uncompressed.payload_variant.chat.to, sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow chat.to. Bailing out");
return;
@@ -176,10 +184,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
if (t->payload_variant.chat.has_to_callsign) {
uncompressed.payload_variant.chat.has_to_callsign = true;
length =
unishox2_decompress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign),
uncompressed.payload_variant.chat.to_callsign,
sizeof(uncompressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL);
length = unishox2_decompress_lines(
t->payload_variant.chat.to_callsign,
pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)),
uncompressed.payload_variant.chat.to_callsign, sizeof(uncompressed.payload_variant.chat.to_callsign) - 1,
USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow chat.to_callsign. Bailing out");
return;

View File

@@ -130,8 +130,7 @@ CannedMessageModule::CannedMessageModule()
: SinglePortModule("canned", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("CannedMessage")
{
this->loadProtoForModule();
if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE &&
!CANNED_MESSAGE_MODULE_ENABLE) {
if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE) {
LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled");
this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED;
disable();

View File

@@ -27,10 +27,6 @@ enum CannedMessageModuleIconType { shift, backspace, space, enter };
#define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50
#define CANNED_MESSAGE_MODULE_MESSAGES_SIZE 800
#ifndef CANNED_MESSAGE_MODULE_ENABLE
#define CANNED_MESSAGE_MODULE_ENABLE 0
#endif
// ============================
// Data Structures
// ============================

View File

@@ -123,7 +123,7 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
// generate nonce
updateState();
if (currentState != KEY_VERIFICATION_IDLE) {
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::throttle_message;)
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;)
return false;
}
currentNonce = random();
@@ -259,7 +259,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
p->priority = meshtastic_MeshPacket_Priority_HIGH;
service->sendToMesh(p, RX_SRC_LOCAL, true);
currentState = KEY_VERIFICATION_SENDER_AWAITING_USER;
IF_SCREEN(screen->requestMenu(graphics::menuHandler::key_verification_final_prompt);)
IF_SCREEN(screen->requestMenu(graphics::menuHandler::KeyVerificationFinalPrompt);)
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_WARNING;
sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message);

View File

@@ -130,7 +130,6 @@ int32_t StatusLEDModule::runOnce()
#ifdef LED_CHARGE
digitalWrite(LED_CHARGE, CHARGE_LED_state);
#endif
// digitalWrite(green_LED_PIN, LED_STATE_OFF);
#ifdef LED_PAIRING
digitalWrite(LED_PAIRING, PAIRING_LED_state);
#endif

View File

@@ -10,7 +10,6 @@
#include "PowerFSM.h"
#include "RTC.h"
#include "Router.h"
#include "Sensor/AddI2CSensorTemplate.h"
#include "UnitConversions.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
@@ -20,7 +19,9 @@
#include <Throttle.h>
// Sensors
#include "Sensor/AddI2CSensorTemplate.h"
#include "Sensor/PMSA003ISensor.h"
#include "Sensor/SEN5XSensor.h"
void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
{
@@ -42,6 +43,7 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
// order by priority of metrics/values (low top, high bottom)
addSensor<PMSA003ISensor>(i2cScanner, ScanI2C::DeviceType::PMSA003I);
addSensor<SEN5XSensor>(i2cScanner, ScanI2C::DeviceType::SEN5X);
}
int32_t AirQualityTelemetryModule::runOnce()
@@ -85,10 +87,27 @@ int32_t AirQualityTelemetryModule::runOnce()
}
// Wake up the sensors that need it
LOG_INFO("Waking up sensors");
LOG_INFO("Waking up sensors...");
for (TelemetrySensor *sensor : sensors) {
if (!sensor->isActive()) {
return sensor->wakeUp();
if (!sensor->canSleep()) {
LOG_DEBUG("%s sensor doesn't have sleep feature. Skipping", sensor->sensorName);
} else if (((lastSentToMesh == 0) ||
!Throttle::isWithinTimespanMs(lastSentToMesh - sensor->wakeUpTimeMs(),
Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
airTime->isTxAllowedAirUtil()) {
if (!sensor->isActive()) {
LOG_DEBUG("Waking up: %s", sensor->sensorName);
return sensor->wakeUp();
} else {
int32_t pendingForReadyMs = sensor->pendingForReadyMs();
LOG_DEBUG("%s. Pending for ready %ums", sensor->sensorName, pendingForReadyMs);
if (pendingForReadyMs) {
return pendingForReadyMs;
}
}
}
}
@@ -109,9 +128,18 @@ int32_t AirQualityTelemetryModule::runOnce()
}
// Send to sleep sensors that consume power
LOG_INFO("Sending sensors to sleep");
LOG_DEBUG("Sending sensors to sleep");
for (TelemetrySensor *sensor : sensors) {
sensor->sleep();
if (sensor->isActive() && sensor->canSleep()) {
if (sensor->wakeUpTimeMs() < Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs,
numOnlineNodes)) {
LOG_DEBUG("Disabling %s until next period", sensor->sensorName);
sensor->sleep();
} else {
LOG_DEBUG("Sensor stays enabled due to warm up period");
}
}
}
}
return min(sendToPhoneIntervalMs, result);
@@ -158,8 +186,7 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta
const auto &m = telemetry.variant.air_quality_metrics;
// Check if any telemetry field has valid data
bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental ||
m.has_pm25_environmental || m.has_pm100_environmental;
bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard;
if (!hasAny) {
display->drawString(x, currentY, "No Telemetry");
@@ -225,9 +252,10 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard,
t->variant.air_quality_metrics.pm100_standard);
LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental,
t->variant.air_quality_metrics.pm100_environmental);
// TODO - Decide what to do with these
// LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
// t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental,
// t->variant.air_quality_metrics.pm100_environmental);
#endif
// release previous packet before occupying a new spot
if (lastMeasurementPacket != nullptr)
@@ -247,10 +275,8 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero;
// TODO - Should we check for sensor state here?
// If a sensor is sleeping, we should know and check to wake it up
for (TelemetrySensor *sensor : sensors) {
LOG_INFO("Reading AQ sensors");
LOG_DEBUG("Reading %s", sensor->sensorName);
valid = valid && sensor->getMetrics(m);
hasSensor = true;
}
@@ -291,12 +317,14 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
m.time = getTime();
if (getAirQualityTelemetry(&m)) {
LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \
pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u",
m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard,
m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental,
m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental);
LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u", m.variant.air_quality_metrics.pm10_standard,
m.variant.air_quality_metrics.pm25_standard, m.variant.air_quality_metrics.pm100_standard);
if (m.variant.air_quality_metrics.has_pm10_environmental)
LOG_INFO("pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u",
m.variant.air_quality_metrics.pm10_environmental, m.variant.air_quality_metrics.pm25_environmental,
m.variant.air_quality_metrics.pm100_environmental);
meshtastic_MeshPacket *p = allocDataProtobuf(m);
p->to = dest;
@@ -331,6 +359,20 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
LOG_DEBUG("Start next execution in 5s, then sleep");
setIntervalFromNow(FIVE_SECONDS_MS);
}
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) {
meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed();
notification->level = meshtastic_LogRecord_Level_INFO;
notification->time = getValidTime(RTCQualityFromNet);
sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment",
Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs) /
1000U);
service->sendClientNotification(notification);
sleepOnNextExecution = true;
LOG_DEBUG("Start next execution in 5s, then sleep");
setIntervalFromNow(FIVE_SECONDS_MS);
}
}
return true;
}

View File

@@ -21,26 +21,29 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
_bus = bus;
_address = dev->address.address;
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus);
if (!currentClock) {
LOG_WARN("PMSA003I can't be used at this clock speed");
return false;
}
#endif
#ifdef PMSA003I_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return false;
#endif /* CAN_RECLOCK_I2C */
#endif /* PMSA003I_I2C_CLOCK_SPEED */
_bus->beginTransmission(_address);
if (_bus->endTransmission() != 0) {
LOG_WARN("PMSA003I not found on I2C at 0x12");
LOG_WARN("%s not found on I2C at 0x12", sensorName);
return false;
}
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus);
reClockI2C(currentClock, _bus, false);
#endif
status = 1;
LOG_INFO("PMSA003I Enabled");
LOG_INFO("%s Enabled", sensorName);
initI2CSensor();
return true;
@@ -49,30 +52,37 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
{
if (!isActive()) {
LOG_WARN("PMSA003I is not active");
LOG_WARN("Can't get metrics. %s is not active", sensorName);
return false;
}
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus);
#endif
#ifdef PMSA003I_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return false;
#endif /* CAN_RECLOCK_I2C */
#endif /* PMSA003I_I2C_CLOCK_SPEED */
_bus->requestFrom(_address, PMSA003I_FRAME_LENGTH);
if (_bus->available() < PMSA003I_FRAME_LENGTH) {
LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", _bus->available());
LOG_WARN("%s read failed: incomplete data (%d bytes)", sensorName, _bus->available());
return false;
}
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus);
#endif
for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) {
buffer[i] = _bus->read();
}
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
if (buffer[0] != 0x42 || buffer[1] != 0x4D) {
LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]);
LOG_WARN("%s frame header invalid: 0x%02X 0x%02X", sensorName, buffer[0], buffer[1]);
return false;
}
@@ -86,7 +96,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2);
if (computedChecksum != receivedChecksum) {
LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum);
LOG_WARN("%s checksum failed: computed 0x%04X, received 0x%04X", sensorName, computedChecksum, receivedChecksum);
return false;
}
@@ -136,20 +146,58 @@ bool PMSA003ISensor::isActive()
return state == State::ACTIVE;
}
int32_t PMSA003ISensor::wakeUpTimeMs()
{
#ifdef PMSA003I_ENABLE_PIN
return PMSA003I_WARMUP_MS;
#endif
return 0;
}
int32_t PMSA003ISensor::pendingForReadyMs()
{
#ifdef PMSA003I_ENABLE_PIN
uint32_t now;
now = getTime();
uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000;
LOG_DEBUG("%s: Since measure started: %ums", sensorName, sincePmMeasureStarted);
if (sincePmMeasureStarted < PMSA003I_WARMUP_MS) {
LOG_INFO("%s: not enough time passed since starting measurement", sensorName);
return PMSA003I_WARMUP_MS - sincePmMeasureStarted;
}
return 0;
#endif
return 0;
}
bool PMSA003ISensor::canSleep()
{
#ifdef PMSA003I_ENABLE_PIN
return true;
#endif
return false;
}
void PMSA003ISensor::sleep()
{
#ifdef PMSA003I_ENABLE_PIN
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
state = State::IDLE;
pmMeasureStarted = 0;
#endif
}
uint32_t PMSA003ISensor::wakeUp()
{
#ifdef PMSA003I_ENABLE_PIN
LOG_INFO("Waking up PMSA003I");
LOG_INFO("Waking up %s", sensorName);
digitalWrite(PMSA003I_ENABLE_PIN, HIGH);
state = State::ACTIVE;
pmMeasureStarted = getTime();
return PMSA003I_WARMUP_MS;
#endif
// No need to wait for warmup if already active

View File

@@ -3,6 +3,7 @@
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "RTC.h"
#include "TelemetrySensor.h"
#define PMSA003I_I2C_CLOCK_SPEED 100000
@@ -19,6 +20,9 @@ class PMSA003ISensor : public TelemetrySensor
virtual bool isActive() override;
virtual void sleep() override;
virtual uint32_t wakeUp() override;
virtual bool canSleep() override;
virtual int32_t wakeUpTimeMs() override;
virtual int32_t pendingForReadyMs() override;
private:
enum class State { IDLE, ACTIVE };
@@ -26,6 +30,7 @@ class PMSA003ISensor : public TelemetrySensor
uint16_t computedChecksum = 0;
uint16_t receivedChecksum = 0;
uint32_t pmMeasureStarted = 0;
uint8_t buffer[PMSA003I_FRAME_LENGTH]{};
TwoWire *_bus{};

View File

@@ -26,7 +26,7 @@ bool RAK12035Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
sensor.get_sensor_version(&data);
if (data != 0) {
LOG_INFO("Init sensor: %s", sensorName);
LOG_INFO("RAK12035Sensor Init Succeed \nSensor1 Firmware version: %i, Sensor Name: %s", data, sensorName);
LOG_INFO("RAK12035Sensor Init Succeed \nSensor Firmware version: %i, Sensor Name: %s", data, sensorName);
status = true;
sensor.sensor_sleep();
RESTORE_3V3_POWER();
@@ -49,33 +49,39 @@ void RAK12035Sensor::setup()
// TODO:: Check for and run calibration check for up to 2 additional sensors if present.
uint16_t zero_val = 0;
uint16_t hundred_val = 0;
uint16_t default_zero_val = 550;
uint16_t default_hundred_val = 420;
const uint16_t default_zero_val = 510;
const uint16_t default_hundred_val = 390;
sensor.sensor_on();
sensor.begin();
delay(200);
sensor.get_dry_cal(&zero_val);
delay(200);
sensor.get_wet_cal(&hundred_val);
delay(200);
if (zero_val == 0 || zero_val <= hundred_val) {
LOG_INFO("Dry calibration value is %d", zero_val);
LOG_INFO("Wet calibration value is %d", hundred_val);
LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: "
"https://github.com/RAKWireless/RAK12035_SoilMoisture.");
LOG_INFO("For now, setting default calibration value for Dry Calibration: %d", default_zero_val);
bool calibrationReset = false;
if (zero_val == 0) {
LOG_INFO("Dry calibration not set, using default: %d", default_zero_val);
sensor.set_dry_cal(default_zero_val);
sensor.get_dry_cal(&zero_val);
LOG_INFO("Dry calibration reset complete. New value is %d", zero_val);
delay(200);
zero_val = default_zero_val;
calibrationReset = true;
}
if (hundred_val == 0 || hundred_val >= zero_val) {
LOG_INFO("Dry calibration value is %d", zero_val);
LOG_INFO("Wet calibration value is %d", hundred_val);
LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: "
"https://github.com/RAKWireless/RAK12035_SoilMoisture.");
LOG_INFO("For now, setting default calibration value for Wet Calibration: %d", default_hundred_val);
LOG_INFO("Wet calibration not set, using default: %d", default_hundred_val);
sensor.set_wet_cal(default_hundred_val);
sensor.get_wet_cal(&hundred_val);
LOG_INFO("Wet calibration reset complete. New value is %d", hundred_val);
delay(200);
hundred_val = default_hundred_val;
calibrationReset = true;
}
if (calibrationReset) {
LOG_INFO("Default calibration values applied. Consider running the calibration sketch for better accuracy: "
"https://github.com/RAKWireless/RAK12035_SoilMoisture");
}
LOG_INFO("Dry calibration value: %d, Wet calibration value: %d", zero_val, hundred_val);
sensor.sensor_sleep();
RESTORE_3V3_POWER();
delay(200);

View File

@@ -0,0 +1,957 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#include "../detect/reClockI2C.h"
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "FSCommon.h"
#include "SEN5XSensor.h"
#include "SPILock.h"
#include "SafeFile.h"
#include "TelemetrySensor.h"
#include <float.h> // FLT_MAX
#include <pb_decode.h>
#include <pb_encode.h>
SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") {}
bool SEN5XSensor::getVersion()
{
if (!sendCommand(SEN5X_GET_FIRMWARE_VERSION)) {
LOG_ERROR("SEN5X: Error sending version command");
return false;
}
delay(20); // From Sensirion Datasheet
uint8_t versionBuffer[12];
size_t charNumber = readBuffer(&versionBuffer[0], 3);
if (charNumber == 0) {
LOG_ERROR("SEN5X: Error getting data ready flag value");
return false;
}
firmwareVer = versionBuffer[0] + (versionBuffer[1] / 10);
hardwareVer = versionBuffer[3] + (versionBuffer[4] / 10);
protocolVer = versionBuffer[5] + (versionBuffer[6] / 10);
LOG_INFO("SEN5X Firmware Version: %0.2f", firmwareVer);
LOG_INFO("SEN5X Hardware Version: %0.2f", hardwareVer);
LOG_INFO("SEN5X Protocol Version: %0.2f", protocolVer);
return true;
}
bool SEN5XSensor::findModel()
{
if (!sendCommand(SEN5X_GET_PRODUCT_NAME)) {
LOG_ERROR("SEN5X: Error asking for product name");
return false;
}
delay(50); // From Sensirion Datasheet
const uint8_t nameSize = 48;
uint8_t name[nameSize];
size_t charNumber = readBuffer(&name[0], nameSize);
if (charNumber == 0) {
LOG_ERROR("SEN5X: Error getting device name");
return false;
}
// We only check the last character that defines the model SEN5X
switch (name[4]) {
case 48:
model = SEN50;
LOG_INFO("SEN5X: found sensor model SEN50");
break;
case 52:
model = SEN54;
LOG_INFO("SEN5X: found sensor model SEN54");
break;
case 53:
model = SEN55;
LOG_INFO("SEN5X: found sensor model SEN55");
break;
}
return true;
}
bool SEN5XSensor::sendCommand(uint16_t command)
{
uint8_t nothing;
return sendCommand(command, &nothing, 0);
}
bool SEN5XSensor::sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber)
{
// At least we need two bytes for the command
uint8_t bufferSize = 2;
// Add space for CRC bytes (one every two bytes)
if (byteNumber > 0)
bufferSize += byteNumber + (byteNumber / 2);
uint8_t toSend[bufferSize];
uint8_t i = 0;
toSend[i++] = static_cast<uint8_t>((command & 0xFF00) >> 8);
toSend[i++] = static_cast<uint8_t>((command & 0x00FF) >> 0);
// Prepare buffer with CRC every third byte
uint8_t bi = 0;
if (byteNumber > 0) {
while (bi < byteNumber) {
toSend[i++] = buffer[bi++];
toSend[i++] = buffer[bi++];
uint8_t calcCRC = sen5xCRC(&buffer[bi - 2]);
toSend[i++] = calcCRC;
}
}
#ifdef SEN5X_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return false;
#endif /* CAN_RECLOCK_I2C */
#endif /* SEN5X_I2C_CLOCK_SPEED */
// Transmit the data
// LOG_DEBUG("Beginning connection to SEN5X: 0x%x. Size: %u", address, bufferSize);
// Note: this delay is necessary to allow for long-buffers
delay(20);
_bus->beginTransmission(_address);
size_t writtenBytes = _bus->write(toSend, bufferSize);
uint8_t i2c_error = _bus->endTransmission();
#if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
if (writtenBytes != bufferSize) {
LOG_ERROR("SEN5X: Error writting on I2C bus");
return false;
}
if (i2c_error != 0) {
LOG_ERROR("SEN5X: Error on I2C communication: %x", i2c_error);
return false;
}
return true;
}
uint8_t SEN5XSensor::readBuffer(uint8_t *buffer, uint8_t byteNumber)
{
#ifdef SEN5X_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return false;
#endif /* CAN_RECLOCK_I2C */
#endif /* SEN5X_I2C_CLOCK_SPEED */
size_t readBytes = _bus->requestFrom(_address, byteNumber);
if (readBytes != byteNumber) {
LOG_ERROR("SEN5X: Error reading I2C bus");
return 0;
}
uint8_t i = 0;
uint8_t receivedBytes = 0;
while (readBytes > 0) {
buffer[i++] = _bus->read(); // Just as a reminder: i++ returns i and after that increments.
buffer[i++] = _bus->read();
uint8_t recvCRC = _bus->read();
uint8_t calcCRC = sen5xCRC(&buffer[i - 2]);
if (recvCRC != calcCRC) {
LOG_ERROR("SEN5X: Checksum error while receiving msg");
return 0;
}
readBytes -= 3;
receivedBytes += 2;
}
#if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return receivedBytes;
}
uint8_t SEN5XSensor::sen5xCRC(uint8_t *buffer)
{
// This code is based on Sensirion's own implementation
// https://github.com/Sensirion/arduino-core/blob/41fd02cacf307ec4945955c58ae495e56809b96c/src/SensirionCrc.cpp
uint8_t crc = 0xff;
for (uint8_t i = 0; i < 2; i++) {
crc ^= buffer[i];
for (uint8_t bit = 8; bit > 0; bit--) {
if (crc & 0x80)
crc = (crc << 1) ^ 0x31;
else
crc = (crc << 1);
}
}
return crc;
}
void SEN5XSensor::sleep()
{
// TODO Check this works
idle(true);
}
bool SEN5XSensor::idle(bool checkState)
{
// From the datasheet:
// By default, the VOC algorithm resets its state to initial
// values each time a measurement is started,
// even if the measurement was stopped only for a short
// time. So, the VOC index output value needs a long time
// until it is stable again. This can be avoided by
// restoring the previously memorized algorithm state before
// starting the measure mode
if (checkState) {
// If the stabilisation period is not passed for SEN54 or SEN55, don't go to idle
if (model != SEN50) {
// Get VOC state before going to idle mode
vocValid = false;
if (vocStateFromSensor()) {
vocValid = vocStateValid();
// Check if we have time, and store it
uint32_t now; // If time is RTCQualityNone, it will return zero
now = getValidTime(RTCQuality::RTCQualityDevice);
if (now) {
// Check if state is valid (non-zero)
vocTime = now;
}
}
if (vocStateStable() && vocValid) {
saveState();
} else {
LOG_INFO("SEN5X: Not stopping measurement, vocState is not stable yet!");
return true;
}
}
}
if (!oneShotMode) {
LOG_INFO("SEN5X: Not stopping measurement, continuous mode!");
return true;
}
// Switch to low-power based on the model
if (model == SEN50) {
if (!sendCommand(SEN5X_STOP_MEASUREMENT)) {
LOG_ERROR("SEN5X: Error stopping measurement");
return false;
}
state = SEN5X_IDLE;
LOG_INFO("SEN5X: Stop measurement mode");
} else {
if (!sendCommand(SEN5X_START_MEASUREMENT_RHT_GAS)) {
LOG_ERROR("SEN5X: Error switching to RHT/Gas measurement");
return false;
}
state = SEN5X_RHTGAS_ONLY;
LOG_INFO("SEN5X: Switch to RHT/Gas only measurement mode");
}
delay(200); // From Sensirion Datasheet
pmMeasureStarted = 0;
return true;
}
bool SEN5XSensor::vocStateRecent(uint32_t now)
{
if (now) {
uint32_t passed = now - vocTime; // in seconds
// Check if state is recent, less than 10 minutes (600 seconds)
if (passed < SEN5X_VOC_VALID_TIME && (now > SEN5X_VOC_VALID_DATE)) {
return true;
}
}
return false;
}
bool SEN5XSensor::vocStateValid()
{
if (!vocState[0] && !vocState[1] && !vocState[2] && !vocState[3] && !vocState[4] && !vocState[5] && !vocState[6] &&
!vocState[7]) {
LOG_DEBUG("SEN5X: VOC state is all 0, invalid");
return false;
} else {
LOG_DEBUG("SEN5X: VOC state is valid");
return true;
}
}
bool SEN5XSensor::vocStateToSensor()
{
if (model == SEN50) {
return true;
}
if (!vocStateValid()) {
LOG_INFO("SEN5X: VOC state is invalid, not sending");
return true;
}
if (!sendCommand(SEN5X_STOP_MEASUREMENT)) {
LOG_ERROR("SEN5X: Error stoping measurement");
return false;
}
delay(200); // From Sensirion Datasheet
LOG_DEBUG("SEN5X: Sending VOC state to sensor");
LOG_DEBUG("[%u, %u, %u, %u, %u, %u, %u, %u]", vocState[0], vocState[1], vocState[2], vocState[3], vocState[4], vocState[5],
vocState[6], vocState[7]);
// Note: send command already takes into account the CRC
// buffer size increment needed
if (!sendCommand(SEN5X_RW_VOCS_STATE, vocState, SEN5X_VOC_STATE_BUFFER_SIZE)) {
LOG_ERROR("SEN5X: Error sending VOC's state command'");
return false;
}
return true;
}
bool SEN5XSensor::vocStateFromSensor()
{
if (model == SEN50) {
return true;
}
LOG_INFO("SEN5X: Getting VOC state from sensor");
// Ask VOCs state from the sensor
if (!sendCommand(SEN5X_RW_VOCS_STATE)) {
LOG_ERROR("SEN5X: Error sending VOC's state command'");
return false;
}
delay(20); // From Sensirion Datasheet
// Retrieve the data
// Allocate buffer to account for CRC
size_t receivedNumber = readBuffer(&vocState[0], SEN5X_VOC_STATE_BUFFER_SIZE + (SEN5X_VOC_STATE_BUFFER_SIZE / 2));
delay(20); // From Sensirion Datasheet
if (receivedNumber == 0) {
LOG_DEBUG("SEN5X: Error getting VOC's state");
return false;
}
// Print the state (if debug is on)
LOG_DEBUG("SEN5X: VOC state retrieved from sensor: [%u, %u, %u, %u, %u, %u, %u, %u]", vocState[0], vocState[1], vocState[2],
vocState[3], vocState[4], vocState[5], vocState[6], vocState[7]);
return true;
}
bool SEN5XSensor::loadState()
{
#ifdef FSCom
spiLock->lock();
auto file = FSCom.open(sen5XStateFileName, FILE_O_READ);
bool okay = false;
if (file) {
LOG_INFO("%s state read from %s", sensorName, sen5XStateFileName);
pb_istream_t stream = {&readcb, &file, meshtastic_SEN5XState_size};
if (!pb_decode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) {
LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream));
} else {
lastCleaning = sen5xstate.last_cleaning_time;
lastCleaningValid = sen5xstate.last_cleaning_valid;
oneShotMode = sen5xstate.one_shot_mode;
if (model != SEN50) {
vocTime = sen5xstate.voc_state_time;
vocValid = sen5xstate.voc_state_valid;
// Unpack state
vocState[7] = (uint8_t)(sen5xstate.voc_state_array >> 56);
vocState[6] = (uint8_t)(sen5xstate.voc_state_array >> 48);
vocState[5] = (uint8_t)(sen5xstate.voc_state_array >> 40);
vocState[4] = (uint8_t)(sen5xstate.voc_state_array >> 32);
vocState[3] = (uint8_t)(sen5xstate.voc_state_array >> 24);
vocState[2] = (uint8_t)(sen5xstate.voc_state_array >> 16);
vocState[1] = (uint8_t)(sen5xstate.voc_state_array >> 8);
vocState[0] = (uint8_t)sen5xstate.voc_state_array;
}
// LOG_DEBUG("Loaded lastCleaning %u", lastCleaning);
// LOG_DEBUG("Loaded lastCleaningValid %u", lastCleaningValid);
// LOG_DEBUG("Loaded oneShotMode %s", oneShotMode ? "true" : "false");
// LOG_DEBUG("Loaded vocTime %u", vocTime);
// LOG_DEBUG("Loaded [%u, %u, %u, %u, %u, %u, %u, %u]",
// vocState[7], vocState[6], vocState[5], vocState[4], vocState[3], vocState[2], vocState[1], vocState[0]);
// LOG_DEBUG("Loaded %svalid VOC state", vocValid ? "" : "in");
okay = true;
}
file.close();
} else {
LOG_INFO("No %s state found (File: %s)", sensorName, sen5XStateFileName);
}
spiLock->unlock();
return okay;
#else
LOG_ERROR("SEN5X: ERROR - Filesystem not implemented");
#endif
}
bool SEN5XSensor::saveState()
{
#ifdef FSCom
auto file = SafeFile(sen5XStateFileName);
sen5xstate.last_cleaning_time = lastCleaning;
sen5xstate.last_cleaning_valid = lastCleaningValid;
sen5xstate.one_shot_mode = oneShotMode;
if (model != SEN50) {
sen5xstate.has_voc_state_time = true;
sen5xstate.has_voc_state_valid = true;
sen5xstate.has_voc_state_array = true;
sen5xstate.voc_state_time = vocTime;
sen5xstate.voc_state_valid = vocValid;
// Unpack state (8 bytes)
sen5xstate.voc_state_array = (((uint64_t)vocState[7]) << 56) | ((uint64_t)vocState[6] << 48) |
((uint64_t)vocState[5] << 40) | ((uint64_t)vocState[4] << 32) |
((uint64_t)vocState[3] << 24) | ((uint64_t)vocState[2] << 16) |
((uint64_t)vocState[1] << 8) | ((uint64_t)vocState[0]);
}
bool okay = false;
LOG_INFO("%s: state write to %s", sensorName, sen5XStateFileName);
pb_ostream_t stream = {&writecb, static_cast<Print *>(&file), meshtastic_SEN5XState_size};
if (!pb_encode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) {
LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream));
} else {
okay = true;
}
okay &= file.close();
if (okay)
LOG_INFO("%s: state write to %s successful", sensorName, sen5XStateFileName);
return okay;
#else
LOG_ERROR("%s: ERROR - Filesystem not implemented", sensorName);
#endif
}
bool SEN5XSensor::isActive()
{
return state == SEN5X_MEASUREMENT || state == SEN5X_MEASUREMENT_2;
}
uint32_t SEN5XSensor::wakeUp()
{
LOG_DEBUG("SEN5X: Waking up sensor");
if (!sendCommand(SEN5X_START_MEASUREMENT)) {
LOG_ERROR("SEN5X: Error starting measurement");
// TODO - what should this return?? Something actually on the default interval?
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
delay(50); // From Sensirion Datasheet
// TODO - This is currently "problematic"
// If time is updated in between reads, there is no way to
// keep track of how long it has passed
pmMeasureStarted = getTime();
state = SEN5X_MEASUREMENT;
if (state == SEN5X_MEASUREMENT)
LOG_INFO("SEN5X: Started measurement mode");
return SEN5X_WARMUP_MS_1;
}
bool SEN5XSensor::vocStateStable()
{
uint32_t now;
now = getTime();
uint32_t sinceFirstMeasureStarted = (now - rhtGasMeasureStarted);
LOG_DEBUG("sinceFirstMeasureStarted: %us", sinceFirstMeasureStarted);
return sinceFirstMeasureStarted > SEN5X_VOC_STATE_WARMUP_S;
}
bool SEN5XSensor::startCleaning()
{
// Note: we only should enter here if we have a valid RTC with at least
// RTCQuality::RTCQualityDevice
state = SEN5X_CLEANING;
// Note that cleaning command can only be run when the sensor is in measurement mode
if (!sendCommand(SEN5X_START_MEASUREMENT)) {
LOG_ERROR("SEN5X: Error starting measurment mode");
return false;
}
delay(50); // From Sensirion Datasheet
if (!sendCommand(SEN5X_START_FAN_CLEANING)) {
LOG_ERROR("SEN5X: Error starting fan cleaning");
return false;
}
delay(20); // From Sensirion Datasheet
// This message will be always printed so the user knows the device it's not hung
LOG_INFO("SEN5X: Started fan cleaning it will take 10 seconds...");
uint16_t started = millis();
while (millis() - started < 10500) {
delay(500);
}
LOG_INFO("SEN5X: Cleaning done!!");
// Save timestamp in flash so we know when a week has passed
uint32_t now;
now = getValidTime(RTCQuality::RTCQualityDevice);
// If time is not RTCQualityNone, it will return non-zero
lastCleaning = now;
lastCleaningValid = true;
saveState();
idle();
return true;
}
bool SEN5XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
{
state = SEN5X_NOT_DETECTED;
LOG_INFO("Init sensor: %s", sensorName);
_bus = bus;
_address = dev->address.address;
delay(50); // without this there is an error on the deviceReset function
if (!sendCommand(SEN5X_RESET)) {
LOG_ERROR("SEN5X: Error reseting device");
return false;
}
delay(200); // From Sensirion Datasheet
if (!findModel()) {
LOG_ERROR("SEN5X: error finding sensor model");
return false;
}
// Check the firmware version
if (!getVersion())
return false;
if (firmwareVer < 2) {
LOG_ERROR("SEN5X: error firmware is too old and will not work with this implementation");
return false;
}
delay(200); // From Sensirion Datasheet
// Detection succeeded
state = SEN5X_IDLE;
status = 1;
// Load state
loadState();
// Check if it is time to do a cleaning
uint32_t now;
int32_t passed;
now = getValidTime(RTCQuality::RTCQualityDevice);
// If time is not RTCQualityNone, it will return non-zero
if (now) {
if (lastCleaningValid) {
passed = now - lastCleaning; // in seconds
if (passed > ONE_WEEK_IN_SECONDS && (now > SEN5X_VOC_VALID_DATE)) {
// If current date greater than 01/01/2018 (validity check)
LOG_INFO("SEN5X: More than a week (%us) since last cleaning in epoch (%us). Trigger, cleaning...", passed,
lastCleaning);
startCleaning();
} else {
LOG_INFO("SEN5X: Cleaning not needed (%ds passed). Last cleaning date (in epoch): %us", passed, lastCleaning);
}
} else {
// We assume the device has just been updated or it is new,
// so no need to trigger a cleaning.
// Just save the timestamp to do a cleaning one week from now.
// Otherwise, we will never trigger cleaning in some cases
lastCleaning = now;
lastCleaningValid = true;
LOG_INFO("SEN5X: No valid last cleaning date found, saving it now: %us", lastCleaning);
saveState();
}
if (model != SEN50) {
if (!vocValid) {
LOG_INFO("SEN5X: No valid VOC's state found");
} else {
// Check if state is recent
if (vocStateRecent(now)) {
// If current date greater than 01/01/2018 (validity check)
// Send it to the sensor
LOG_INFO("SEN5X: VOC state is valid and recent");
vocStateToSensor();
} else {
LOG_INFO("SEN5X: VOC state is too old or date is invalid");
LOG_DEBUG("SEN5X: vocTime %u, Passed %u, and now %u", vocTime, passed, now);
}
}
}
} else {
// TODO - Should this actually ignore? We could end up never cleaning...
LOG_INFO("SEN5X: Not enough RTCQuality, ignoring saved state. Trying again later");
}
idle(false);
rhtGasMeasureStarted = now;
initI2CSensor();
return true;
}
bool SEN5XSensor::readValues()
{
if (!sendCommand(SEN5X_READ_VALUES)) {
LOG_ERROR("SEN5X: Error sending read command");
return false;
}
LOG_DEBUG("SEN5X: Reading PM Values");
delay(20); // From Sensirion Datasheet
uint8_t dataBuffer[16];
size_t receivedNumber = readBuffer(&dataBuffer[0], 24);
if (receivedNumber == 0) {
LOG_ERROR("SEN5X: Error getting values");
return false;
}
// Get the integers
uint16_t uint_pM1p0 = static_cast<uint16_t>((dataBuffer[0] << 8) | dataBuffer[1]);
uint16_t uint_pM2p5 = static_cast<uint16_t>((dataBuffer[2] << 8) | dataBuffer[3]);
uint16_t uint_pM4p0 = static_cast<uint16_t>((dataBuffer[4] << 8) | dataBuffer[5]);
uint16_t uint_pM10p0 = static_cast<uint16_t>((dataBuffer[6] << 8) | dataBuffer[7]);
int16_t int_humidity = static_cast<int16_t>((dataBuffer[8] << 8) | dataBuffer[9]);
int16_t int_temperature = static_cast<int16_t>((dataBuffer[10] << 8) | dataBuffer[11]);
int16_t int_vocIndex = static_cast<int16_t>((dataBuffer[12] << 8) | dataBuffer[13]);
int16_t int_noxIndex = static_cast<int16_t>((dataBuffer[14] << 8) | dataBuffer[15]);
// Convert values based on Sensirion Arduino lib
sen5xmeasurement.pM1p0 = !isnan(uint_pM1p0) ? uint_pM1p0 / 10 : UINT16_MAX;
sen5xmeasurement.pM2p5 = !isnan(uint_pM2p5) ? uint_pM2p5 / 10 : UINT16_MAX;
sen5xmeasurement.pM4p0 = !isnan(uint_pM4p0) ? uint_pM4p0 / 10 : UINT16_MAX;
sen5xmeasurement.pM10p0 = !isnan(uint_pM10p0) ? uint_pM10p0 / 10 : UINT16_MAX;
sen5xmeasurement.humidity = !isnan(int_humidity) ? int_humidity / 100.0f : FLT_MAX;
sen5xmeasurement.temperature = !isnan(int_temperature) ? int_temperature / 200.0f : FLT_MAX;
sen5xmeasurement.vocIndex = !isnan(int_vocIndex) ? int_vocIndex / 10.0f : FLT_MAX;
sen5xmeasurement.noxIndex = !isnan(int_noxIndex) ? int_noxIndex / 10.0f : FLT_MAX;
LOG_DEBUG("Got: pM1p0=%u, pM2p5=%u, pM4p0=%u, pM10p0=%u", sen5xmeasurement.pM1p0, sen5xmeasurement.pM2p5,
sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0);
if (model != SEN50) {
LOG_DEBUG("Got: humidity=%.2f, temperature=%.2f, vocIndex=%.2f", sen5xmeasurement.humidity, sen5xmeasurement.temperature,
sen5xmeasurement.vocIndex);
}
if (model == SEN55) {
LOG_DEBUG("Got: noxIndex=%.2f", sen5xmeasurement.noxIndex);
}
return true;
}
bool SEN5XSensor::readPNValues(bool cumulative)
{
if (!sendCommand(SEN5X_READ_PM_VALUES)) {
LOG_ERROR("SEN5X: Error sending read command");
return false;
}
LOG_DEBUG("SEN5X: Reading PN Values");
delay(20); // From Sensirion Datasheet
uint8_t dataBuffer[20];
size_t receivedNumber = readBuffer(&dataBuffer[0], 30);
if (receivedNumber == 0) {
LOG_ERROR("SEN5X: Error getting PN values");
return false;
}
// Get the integers
// uint16_t uint_pM1p0 = static_cast<uint16_t>((dataBuffer[0] << 8) | dataBuffer[1]);
// uint16_t uint_pM2p5 = static_cast<uint16_t>((dataBuffer[2] << 8) | dataBuffer[3]);
// uint16_t uint_pM4p0 = static_cast<uint16_t>((dataBuffer[4] << 8) | dataBuffer[5]);
// uint16_t uint_pM10p0 = static_cast<uint16_t>((dataBuffer[6] << 8) | dataBuffer[7]);
uint16_t uint_pN0p5 = static_cast<uint16_t>((dataBuffer[8] << 8) | dataBuffer[9]);
uint16_t uint_pN1p0 = static_cast<uint16_t>((dataBuffer[10] << 8) | dataBuffer[11]);
uint16_t uint_pN2p5 = static_cast<uint16_t>((dataBuffer[12] << 8) | dataBuffer[13]);
uint16_t uint_pN4p0 = static_cast<uint16_t>((dataBuffer[14] << 8) | dataBuffer[15]);
uint16_t uint_pN10p0 = static_cast<uint16_t>((dataBuffer[16] << 8) | dataBuffer[17]);
uint16_t uint_tSize = static_cast<uint16_t>((dataBuffer[18] << 8) | dataBuffer[19]);
// Convert values based on Sensirion Arduino lib
// Multiply by 100 for converting from #/cm3 to #/0.1l for PN values
sen5xmeasurement.pN0p5 = !isnan(uint_pN0p5) ? uint_pN0p5 / 10 * 100 : UINT32_MAX;
sen5xmeasurement.pN1p0 = !isnan(uint_pN1p0) ? uint_pN1p0 / 10 * 100 : UINT32_MAX;
sen5xmeasurement.pN2p5 = !isnan(uint_pN2p5) ? uint_pN2p5 / 10 * 100 : UINT32_MAX;
sen5xmeasurement.pN4p0 = !isnan(uint_pN4p0) ? uint_pN4p0 / 10 * 100 : UINT32_MAX;
sen5xmeasurement.pN10p0 = !isnan(uint_pN10p0) ? uint_pN10p0 / 10 * 100 : UINT32_MAX;
sen5xmeasurement.tSize = !isnan(uint_tSize) ? uint_tSize / 1000.0f : FLT_MAX;
// Remove accumuluative values:
// https://github.com/fablabbcn/smartcitizen-kit-2x/issues/85
if (!cumulative) {
sen5xmeasurement.pN10p0 -= sen5xmeasurement.pN4p0;
sen5xmeasurement.pN4p0 -= sen5xmeasurement.pN2p5;
sen5xmeasurement.pN2p5 -= sen5xmeasurement.pN1p0;
sen5xmeasurement.pN1p0 -= sen5xmeasurement.pN0p5;
}
LOG_DEBUG("Got: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f", sen5xmeasurement.pN0p5,
sen5xmeasurement.pN1p0, sen5xmeasurement.pN2p5, sen5xmeasurement.pN4p0, sen5xmeasurement.pN10p0,
sen5xmeasurement.tSize);
return true;
}
uint8_t SEN5XSensor::getMeasurements()
{
uint32_t now;
now = getTime();
// Try to get new data
if (!sendCommand(SEN5X_READ_DATA_READY)) {
LOG_ERROR("SEN5X: Error sending command data ready flag");
return 2;
}
delay(20); // From Sensirion Datasheet
uint8_t dataReadyBuffer[3];
size_t charNumber = readBuffer(&dataReadyBuffer[0], 3);
if (charNumber == 0) {
LOG_ERROR("SEN5X: Error getting device version value");
return 2;
}
bool dataReady = dataReadyBuffer[1];
uint32_t sinceLastDataPollMs = (now - lastDataPoll) * 1000;
// Check if data is ready, and if since last time we requested is less than SEN5X_POLL_INTERVAL
if (!dataReady && (sinceLastDataPollMs > SEN5X_POLL_INTERVAL)) {
LOG_INFO("SEN5X: Data is not ready");
return 1;
}
if (!readValues()) {
LOG_ERROR("SEN5X: Error getting readings");
return 2;
}
if (!readPNValues(false)) {
LOG_ERROR("SEN5X: Error getting PN readings");
return 2;
}
lastDataPoll = now;
return 0;
}
int32_t SEN5XSensor::wakeUpTimeMs()
{
return SEN5X_WARMUP_MS_2;
}
int32_t SEN5XSensor::pendingForReadyMs()
{
uint32_t now;
now = getTime();
uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000;
LOG_DEBUG("SEN5X: Since measure started: %ums", sincePmMeasureStarted);
switch (state) {
case SEN5X_MEASUREMENT: {
if (sincePmMeasureStarted < SEN5X_WARMUP_MS_1) {
LOG_INFO("SEN5X: not enough time passed since starting measurement");
return SEN5X_WARMUP_MS_1 - sincePmMeasureStarted;
}
if (!pmMeasureStarted) {
pmMeasureStarted = now;
}
// Get PN values to check if we are above or below threshold
readPNValues(true);
lastDataPoll = now;
// If the reading is low (the tyhreshold is in #/cm3) and second warmUp hasn't passed we return to come back later
if ((sen5xmeasurement.pN4p0 / 100) < SEN5X_PN4P0_CONC_THD && sincePmMeasureStarted < SEN5X_WARMUP_MS_2) {
LOG_INFO("SEN5X: Concentration is low, we will ask again in the second warm up period");
state = SEN5X_MEASUREMENT_2;
// Report how many seconds are pending to cover the first warm up period
return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted;
}
return 0;
}
case SEN5X_MEASUREMENT_2: {
if (sincePmMeasureStarted < SEN5X_WARMUP_MS_2) {
// Report how many seconds are pending to cover the first warm up period
return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted;
}
return 0;
}
default: {
return -1;
}
}
}
bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement)
{
LOG_INFO("SEN5X: Attempting to get metrics");
if (!isActive()) {
LOG_INFO("SEN5X: not in measurement mode");
return false;
}
uint8_t response;
response = getMeasurements();
if (response == 0) {
if (sen5xmeasurement.pM1p0 != UINT16_MAX) {
measurement->variant.air_quality_metrics.has_pm10_standard = true;
measurement->variant.air_quality_metrics.pm10_standard = sen5xmeasurement.pM1p0;
}
if (sen5xmeasurement.pM2p5 != UINT16_MAX) {
measurement->variant.air_quality_metrics.has_pm25_standard = true;
measurement->variant.air_quality_metrics.pm25_standard = sen5xmeasurement.pM2p5;
}
if (sen5xmeasurement.pM4p0 != UINT16_MAX) {
measurement->variant.air_quality_metrics.has_pm40_standard = true;
measurement->variant.air_quality_metrics.pm40_standard = sen5xmeasurement.pM4p0;
}
if (sen5xmeasurement.pM10p0 != UINT16_MAX) {
measurement->variant.air_quality_metrics.has_pm100_standard = true;
measurement->variant.air_quality_metrics.pm100_standard = sen5xmeasurement.pM10p0;
}
if (sen5xmeasurement.pN0p5 != UINT32_MAX) {
measurement->variant.air_quality_metrics.has_particles_05um = true;
measurement->variant.air_quality_metrics.particles_05um = sen5xmeasurement.pN0p5;
}
if (sen5xmeasurement.pN1p0 != UINT32_MAX) {
measurement->variant.air_quality_metrics.has_particles_10um = true;
measurement->variant.air_quality_metrics.particles_10um = sen5xmeasurement.pN1p0;
}
if (sen5xmeasurement.pN2p5 != UINT32_MAX) {
measurement->variant.air_quality_metrics.has_particles_25um = true;
measurement->variant.air_quality_metrics.particles_25um = sen5xmeasurement.pN2p5;
}
if (sen5xmeasurement.pN4p0 != UINT32_MAX) {
measurement->variant.air_quality_metrics.has_particles_40um = true;
measurement->variant.air_quality_metrics.particles_40um = sen5xmeasurement.pN4p0;
}
if (sen5xmeasurement.pN10p0 != UINT32_MAX) {
measurement->variant.air_quality_metrics.has_particles_100um = true;
measurement->variant.air_quality_metrics.particles_100um = sen5xmeasurement.pN10p0;
}
if (sen5xmeasurement.tSize != FLT_MAX) {
measurement->variant.air_quality_metrics.has_particles_tps = true;
measurement->variant.air_quality_metrics.particles_tps = sen5xmeasurement.tSize;
}
if (model != SEN50) {
if (sen5xmeasurement.humidity != FLT_MAX) {
measurement->variant.air_quality_metrics.has_pm_humidity = true;
measurement->variant.air_quality_metrics.pm_humidity = sen5xmeasurement.humidity;
}
if (sen5xmeasurement.temperature != FLT_MAX) {
measurement->variant.air_quality_metrics.has_pm_temperature = true;
measurement->variant.air_quality_metrics.pm_temperature = sen5xmeasurement.temperature;
}
if (sen5xmeasurement.noxIndex != FLT_MAX) {
measurement->variant.air_quality_metrics.has_pm_voc_idx = true;
measurement->variant.air_quality_metrics.pm_voc_idx = sen5xmeasurement.vocIndex;
}
}
if (model == SEN55) {
if (sen5xmeasurement.noxIndex != FLT_MAX) {
measurement->variant.air_quality_metrics.has_pm_nox_idx = true;
measurement->variant.air_quality_metrics.pm_nox_idx = sen5xmeasurement.noxIndex;
}
}
return true;
} else if (response == 1) {
// TODO return because data was not ready yet
// Should this return false?
idle();
return false;
} else if (response == 2) {
// Return with error for non-existing data
idle();
return false;
}
return true;
}
void SEN5XSensor::setMode(bool setOneShot)
{
oneShotMode = setOneShot;
}
AdminMessageHandleResult SEN5XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response)
{
AdminMessageHandleResult result;
result = AdminMessageHandleResult::NOT_HANDLED;
switch (request->which_payload_variant) {
case meshtastic_AdminMessage_sensor_config_tag:
if (!request->sensor_config.has_sen5x_config) {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
// TODO - Add admin command to set temperature offset
// Check for temperature offset
// if (request->sensor_config.sen5x_config.has_set_temperature) {
// this->setTemperature(request->sensor_config.sen5x_config.set_temperature);
// }
// Check for one-shot/continuous mode request
if (request->sensor_config.sen5x_config.has_set_one_shot_mode) {
this->setMode(request->sensor_config.sen5x_config.set_one_shot_mode);
}
result = AdminMessageHandleResult::HANDLED;
break;
default:
result = AdminMessageHandleResult::NOT_HANDLED;
}
return result;
}
#endif

View File

@@ -0,0 +1,170 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "RTC.h"
#include "TelemetrySensor.h"
#include "Wire.h"
// Warm up times for SEN5X from the datasheet
#ifndef SEN5X_WARMUP_MS_1
#define SEN5X_WARMUP_MS_1 15000
#endif
#ifndef SEN5X_WARMUP_MS_2
#define SEN5X_WARMUP_MS_2 30000
#endif
#ifndef SEN5X_POLL_INTERVAL
#define SEN5X_POLL_INTERVAL 1000
#endif
#ifndef SEN5X_I2C_CLOCK_SPEED
#define SEN5X_I2C_CLOCK_SPEED 100000
#endif
/*
Time after which the sensor can go to sleep, as the warmup period has passed
and the VOCs sensor will is allowed to stop (although needs to recover the state
each time)
*/
#ifndef SEN5X_VOC_STATE_WARMUP_S
/* Note for Testing 5' is enough
Sensirion recommends 1h
This can be bypassed completely if switching to low-power RHT/Gas mode and setting
SEN5X_VOC_STATE_WARMUP_S 0
*/
#define SEN5X_VOC_STATE_WARMUP_S 3600
#endif
#define ONE_WEEK_IN_SECONDS 604800
struct _SEN5XMeasurements {
uint16_t pM1p0;
uint16_t pM2p5;
uint16_t pM4p0;
uint16_t pM10p0;
uint32_t pN0p5;
uint32_t pN1p0;
uint32_t pN2p5;
uint32_t pN4p0;
uint32_t pN10p0;
float tSize;
float humidity;
float temperature;
float vocIndex;
float noxIndex;
};
class SEN5XSensor : public TelemetrySensor
{
private:
TwoWire *_bus{};
uint8_t _address{};
bool getVersion();
float firmwareVer = -1;
float hardwareVer = -1;
float protocolVer = -1;
bool findModel();
// Commands
#define SEN5X_RESET 0xD304
#define SEN5X_GET_PRODUCT_NAME 0xD014
#define SEN5X_GET_FIRMWARE_VERSION 0xD100
#define SEN5X_START_MEASUREMENT 0x0021
#define SEN5X_START_MEASUREMENT_RHT_GAS 0x0037
#define SEN5X_STOP_MEASUREMENT 0x0104
#define SEN5X_READ_DATA_READY 0x0202
#define SEN5X_START_FAN_CLEANING 0x5607
#define SEN5X_RW_VOCS_STATE 0x6181
#define SEN5X_READ_VALUES 0x03C4
#define SEN5X_READ_RAW_VALUES 0x03D2
#define SEN5X_READ_PM_VALUES 0x0413
#define SEN5X_VOC_VALID_TIME 600
#define SEN5X_VOC_VALID_DATE 1514764800
enum SEN5Xmodel { SEN5X_UNKNOWN = 0, SEN50 = 0b001, SEN54 = 0b010, SEN55 = 0b100 };
SEN5Xmodel model = SEN5X_UNKNOWN;
enum SEN5XState {
SEN5X_OFF,
SEN5X_IDLE,
SEN5X_RHTGAS_ONLY,
SEN5X_MEASUREMENT,
SEN5X_MEASUREMENT_2,
SEN5X_CLEANING,
SEN5X_NOT_DETECTED
};
SEN5XState state = SEN5X_OFF;
// Flag to work on one-shot (read and sleep), or continuous mode
bool oneShotMode = true;
void setMode(bool setOneShot);
bool vocStateValid();
/* Sensirion recommends taking a reading after 15 seconds,
if the Particle number reading is over 100#/cm3 the reading is OK,
but if it is lower wait until 30 seconds and take it again.
See: https://sensirion.com/resource/application_note/low_power_mode/sen5x
*/
#define SEN5X_PN4P0_CONC_THD 100
bool sendCommand(uint16_t command);
bool sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber = 0);
uint8_t readBuffer(uint8_t *buffer, uint8_t byteNumber); // Return number of bytes received
uint8_t sen5xCRC(uint8_t *buffer);
bool startCleaning();
uint8_t getMeasurements();
// bool readRawValues();
bool readPNValues(bool cumulative);
bool readValues();
uint32_t pmMeasureStarted = 0;
uint32_t rhtGasMeasureStarted = 0;
uint32_t lastDataPoll = 0;
_SEN5XMeasurements sen5xmeasurement{};
bool idle(bool checkState = true);
protected:
// Store status of the sensor in this file
const char *sen5XStateFileName = "/prefs/sen5X.dat";
meshtastic_SEN5XState sen5xstate = meshtastic_SEN5XState_init_zero;
bool loadState();
bool saveState();
// Cleaning State
uint32_t lastCleaning = 0;
bool lastCleaningValid = false;
// VOC State
#define SEN5X_VOC_STATE_BUFFER_SIZE 8
uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE]{};
uint32_t vocTime = 0;
bool vocValid = false;
bool vocStateFromSensor();
bool vocStateToSensor();
bool vocStateStable();
bool vocStateRecent(uint32_t now);
public:
SEN5XSensor();
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
virtual bool isActive() override;
virtual void sleep() override;
virtual uint32_t wakeUp() override;
virtual bool canSleep() override { return true; }
virtual int32_t wakeUpTimeMs() override;
virtual int32_t pendingForReadyMs() override;
AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response) override;
};
#endif

View File

@@ -26,7 +26,6 @@ class TelemetrySensor
this->status = 0;
}
const char *sensorName;
meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET;
unsigned status;
bool initialized = false;
@@ -56,13 +55,18 @@ class TelemetrySensor
return AdminMessageHandleResult::NOT_HANDLED;
}
const char *sensorName;
// TODO: delete after migration
bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; }
// Functions to sleep / wakeup sensors that support it
// These functions can save power consumption in cases like AQ
virtual void sleep(){};
virtual uint32_t wakeUp() { return 0; }
// Return active by default, override per sensor
virtual bool isActive() { return true; }
virtual bool isActive() { return true; } // Return true by default, override per sensor
virtual bool canSleep() { return false; } // Return false by default, override per sensor
virtual int32_t wakeUpTimeMs() { return 0; }
virtual int32_t pendingForReadyMs() { return 0; }
#if WIRE_INTERFACES_COUNT > 1
// Set to true if Implementation only works first I2C port (Wire)

View File

@@ -33,9 +33,6 @@
#ifndef HAS_RADIO
#define HAS_RADIO 1
#endif
#ifndef HAS_RTC
#define HAS_RTC 1
#endif
#ifndef HAS_CPU_SHUTDOWN
#define HAS_CPU_SHUTDOWN 1
#endif

View File

@@ -24,6 +24,11 @@
#include <nvs.h>
#include <nvs_flash.h>
// Weak empty variant shutdown prep function.
// May be redefined by variant files.
void variant_shutdown() __attribute__((weak));
void variant_shutdown() {}
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH
void setBluetoothEnable(bool enable)
{
@@ -249,6 +254,7 @@ void cpuDeepSleep(uint32_t msecToWake)
#endif // #end ESP32S3_WAKE_TYPE
#endif
variant_shutdown();
// We want RTC peripherals to stay on
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);

View File

@@ -36,6 +36,13 @@ bool AsyncUDP::writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t p
return udp.endPacket();
}
void AsyncUDP::close()
{
udp.stop();
localPort = 0;
_onPacket = nullptr;
}
// AsyncUDPPacket
AsyncUDPPacket::AsyncUDPPacket(EthernetUDP &source) : _udp(source), _remoteIP(source.remoteIP()), _remotePort(source.remotePort())
{

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