Compare commits

..

141 Commits

Author SHA1 Message Date
Jonathan Bennett
00a5fb40b4 Merge branch 'develop' into pin_led 2026-01-30 17:31:16 -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
Jonathan Bennett
8073ea7111 Merge branch 'develop' into pin_led 2026-01-30 13:44:54 -06:00
Jonathan Bennett
486a7f98b6 Deprecate BLE_LED_INVERTED 2026-01-30 13:43:13 -06:00
Jonathan Bennett
8f630bfcf3 Fix typo in LED state comment
And removed unused define
2026-01-30 12:52:17 -06:00
Jonathan Bennett
a6397f3e72 Don't ever define PIN_LED 2026-01-30 12:39:47 -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
renovate[bot]
b18742c211 Update libch341-spi-userspace digest to af9bc27 (#9472)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 06:25:07 -06:00
Ben Meadors
03084f6d3b PRs with needs-review still should get bot labeled 2026-01-29 06:23:52 -06:00
Ben Meadors
94d7b71aa8 Merge branch 'develop' 2026-01-29 06:12:11 -06:00
Ben Meadors
415686dd06 Trunk 2026-01-29 05:56:19 -06:00
Quency-D
b2f2f6b305 Add a watchdog module to meshsolar. (#9337)
* add watchdog module

* Restore the code in power.h

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-28 20:41:27 -06:00
Jonathan Bennett
df400850c1 Undefine LED_BUILTIN where needed 2026-01-28 18:56:57 -06:00
Jonathan Bennett
6ab2f02dbc re-add unintentionally removed include 2026-01-28 17:53:12 -06:00
oscgonfer
d7d6fe7f0f Avoid short-circuit evaluation issues in Telemetry (#9467)
* Make sensors in telemetry non-definitory

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-28 17:18:42 -06:00
Ben Meadors
d44ceb6eb2 Fix NimBLE deinit null check 2026-01-28 17:17:35 -06:00
Jonathan Bennett
4fd0a8276b Just set LED_BUILTIN universally to -1, as we don't use it. (#8830)
* Just set LED_BUILTIN universally to -1, as we don't use it.

* LUD_BUILTIN workarounds

* Squash the LED_BUILTINs that sneaked in

* Don't kill valid pin derfine
2026-01-28 17:09:13 -06:00
Jonathan Bennett
1d219a93ab Move input init to an init function in InputBroker (#9463)
* Move input init to an init function in iInputBroker

* Unbreak targets with EXCLUDE_INPUTBROKER

* Update src/input/InputBroker.cpp

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

* Fix conditional compilation for input broker

* Apply suggestions from code review

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

* Trunk

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 14:58:05 -06:00
Eric Severance
f710cd6ecb Support fully direct request/responses (#9455)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-28 14:14:01 -06:00
Jonathan Bennett
571c1ac34c Initial serialModule cleanup (#9465)
* Initial serialModule cleanup

* Move SERIAL_PRINT_PORT definition to variant.h

* Add missed c6 check

* Update src/modules/SerialModule.cpp

Compile error for invalid SERIAL_PRINT_OBJECT value

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 14:08:32 -06:00
Ben Meadors
fb635987d1 Merge remote-tracking branch 'origin/master' into develop 2026-01-28 13:31:39 -06:00
Jason P
a922751afc External Notification - handleReceived Rewrite (#9454)
* First steps in consolidating code and minimizing rewrite

* Continuing code cleanup

* Merge containsBell and !isMuted to a single code path

* Forgot about alert_message_buzzer in the cleanup

* More code refinements and cleanup

* Fix nagCycleCutoff

* CoPilot Updates
2026-01-28 11:12:02 -06:00
renovate[bot]
c1e3f56324 Update LovyanGFX to v1.2.19 (#9405)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-28 11:08:07 -06:00
Ben Meadors
d0562e1ee6 Add model workflows (#9462)
* Add GitHub workflows for issue completeness, duplicate detection, onboarding, and contribution quality checks

* Fix indentation

* Refactor GitHub workflows for issue handling

* Consolidate to two triage workflows

* Update .github/workflows/models_pr_triage.yml

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 10:50:56 -06:00
Jason P
4eb4c4b584 BaseUI Message Bubble Improvements (#9452)
* Improve Message bubbles for more distinct markers and improved layout

* Tune message bubble size and corner markers

* Finish message bubble tuning

---------

Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2026-01-27 21:11:56 -05:00
Jonathan Bennett
69a42e1fd2 Add portduino_status, assign hardware device IDs... (#9441)
* Add portduino_status, assign hardware device IDs, and try to recover a CH341 device on a USB error

* Minor fixes suggested by Copilot
2026-01-27 18:00:20 -06:00
Jonathan Bennett
fd498bebad Add support for Hackaday Communicator function keys (#9444) 2026-01-27 16:09:18 -06:00
小林
23a8b5a66f Fix uMesh RF POWER configuration error (#9326)
* fix issue https://github.com/linser233/uMesh/issues/1

* fix issue https://github.com/linser233/uMesh/issues/1

* Update and rename lora-usb-umesh-1262.yaml to lora-usb-umesh-1262-30dbm.yaml

* Update and rename lora-usb-umesh-1268.yaml to lora-usb-umesh-1268-30dbm.yaml
2026-01-28 07:50:50 +11:00
Ben Meadors
e1e8d6124d Merge branch 'master' into develop 2026-01-27 14:01:27 -06:00
Jonathan Bennett
10b2eae70c Move more code out of main-nrf52 into variant.cpp (#9450) 2026-01-27 13:56:32 -06:00
github-actions[bot]
cfda9bb8ef Update protobufs (#9453)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-01-27 13:12:03 -06:00
renovate[bot]
d1edd386b6 Update meshtastic/device-ui digest to 69739b8 (#9448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 13:08:10 -06:00
Ben Meadors
b6a1020fc5 Add error handling for SPI command failures in LR11x0, RF95, and SX128x interfaces (#9447) 2026-01-27 13:06:50 -06:00
Jonathan Bennett
91ad861b26 Add Thinknode M4 variant_shutdown() (#9449) 2026-01-27 09:56:56 -06:00
HarukiToreda
d54ae5dad8 InkHUD Menu improvements (#8975)
* InkHUD: Region Picker on initial setup

* Added Node Config menu with Lora Region Picker

* Role picker

* Preset Picker

* Timezone picker added

* Power save mode and bluetooth configs

* Config section Headers

* Channel Config

* Cleaning some behavior

* Add back to all Options

* Display config added

* Position Toggle added

* Network Config for ESP32

* Wifi details

* Reduce line spacing to fit more content

* Recent list with checkboxes

* Timezone labels easier to understand

* Trunk fix

* Added "Saving Changes" screen when reboot is needed

* Trunk fix

* Make Tips show after first boot if the region is Unset

* Added ResetDB and keep only favorite commands

* quick fix to joystick

* Trunk Fix

* Fix to tips to work with new joystick input

* Added ADC multiplier value display on power config

* added ADC calibration feature

* Fixed missing stray endiff

* GPS toggle now is aware if gps is present.
2026-01-27 09:11:11 -06:00
Ben Meadors
c8079d4115 Metadata for heltec tracker v2 2026-01-27 08:05:36 -06:00
Andrew Yong
90778a4e78 feat(GPS): Support Softsleep with WAKE-UP pin on PA1010D (#9078)
Support softsleep by defining PIN_GPS_STANDBY on CDTop CD-PA1010D.

This differs from existing MTK GPS e.g. L76K, where pulling PIN_GPS_STANDBY (WAKE-UP pin) low is not sufficient to put the GPS module in standby.

An additional `$PMTK225,4*2F` must be sent to enter "Backup Mode", which is exited by bringing PIN_GPS_STANDBY (WAKE-UP pin) high.

Refer to datasheet[0] §1.9.3 "Backup Mode".

0: https://cdn-learn.adafruit.com/assets/assets/000/084/295/original/CD_PA1010D_Datasheet_v.03.pdf

Signed-off-by: Andrew Yong <me@ndoo.sg>
2026-01-27 06:03:31 -06:00
Colby Dillion
63a97a54e1 Fix retry_delay calculation for error responses (#9443)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-26 18:45:24 -06:00
Jonathan Bennett
a2e8e232f1 Remove the unused OCV_ARRAYs and move one to a variant.h (#9442) 2026-01-26 16:58:16 -06:00
Keane
7efc3e3770 Replace strcpy with strncpy and null termination (#9436)
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2026-01-26 14:32:03 -06:00
Jonathan Bennett
3d58c6e916 Trackball revamp (#9440)
* Trackball revamp

* Use Throttle

* Volatile!
2026-01-26 14:28:05 -06:00
Jonathan Bennett
c038cfe69a Move device code from main.cpp to earlyInitVariant (#9438) 2026-01-26 11:54:14 -06:00
Ben Meadors
0770f25e79 Merge remote-tracking branch 'origin/master' into develop 2026-01-26 08:56:38 -06:00
Jonathan Bennett
8a9830282a Move Lora Init code into LoraInit.cpp/h (#9435)
* Move Lora Init code into LoraInit.cpp/h

* Add missed extern definition

* More carefully check for nullptr in LoraInit.cpp

* STM32 fixes

* Add extern SPI1 for HW_SPI1_DEVICE

* Move initLora to RadioInterface.h
2026-01-25 20:54:17 -06:00
Ben Meadors
8894a0b711 Consolidate LoRa params / preset logic and fix display of preset values (#9413)
* Consolidate LoRa params / preset logic and fix display of preset values

* Move preset coercion logic to RadioInterface

* Fix some warnings

* Fix warnings

* STM32 fix

* Add unit tests

* Update src/mesh/RadioInterface.h

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-25 18:02:26 -06:00
phaseloop
57a3ff8dfc NRF52 - power management improvements (#9211)
* minor NRF52 test cleanup

* detect USB power input on ProMicro boards

* prevent booting on power failure detection

* introduce PowerHAL layer

* powerHAL basic implementation for NRF52

* prevent data saves on low power

* remove comment

* Update src/platform/nrf52/main-nrf52.cpp

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

* Update src/power/PowerHAL.h

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

* Update src/main.cpp

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

* Merge missing voltage threshold comparison

* add missing variable

* add missing function declaration

* remove debug strings

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-24 08:39:03 -06:00
Uğur ALTINSOY
6cff13623f Added Minimesh variant (#9289)
* Minimesh Lite Added

* Add Minimesh Lite NRF

* Added board_level = extra

* Fix formatting and optimize image for Minimesh Lite

* Change image

* The image has been deleted.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-24 08:38:07 -06:00
phaseloop
b312f226b4 Cut NRF52 bluetooth power usage (#8992)
* Improve NRF52 bluetooth power efficiency

* test T114 bad LFXO

* T1000 test

* force BLE param negotiation

* stash

* NRF52 bluetooth small cleanup

* fix potential connectivity issues

* lower BLE min interval to make iOS happy

* remove slave latency negotation

* add BLE issue comment

* code format

* Revert "code format"

This reverts commit 1f92b09d08.

* remove LFCLK debug info

* Fix

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-24 06:22:01 -06:00
renovate[bot]
b627fa720b Update SensorLib to v0.3.4 (#9396)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-24 05:46:46 -06:00
renovate[bot]
9faf178bdc Update XPowersLib to v0.3.3 (#9354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-24 05:45:26 -06:00
renovate[bot]
c98f134b40 Update meshtastic-esp32_https_server digest to b0f3960 (#9393)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-24 05:39:22 -06:00
renovate[bot]
5838b26d90 Update lewisxhe-SensorLib to v0.3.4 (#9395)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-24 05:39:05 -06:00
Eric Sesterhenn
7221fc4d4b Delete unused code (#9350)
* Delete unused code

CryptoEngine::clearKeys() is not used in the code base, therefore this
cleanup removes the code. It might give casual reviewers the impression,
that keys are wiped.

Since the code uses memset() which might be optimized away by the
compiler, using the code might not even cause the memory
to be wiped.

* Update CryptoEngine.cpp

Fix stray newline, this is the only thing that I can come up with that might confuse the linter.

---------

Co-authored-by: Jason P <applewiz@mac.com>
2026-01-24 05:19:19 -06:00
renovate[bot]
a417760887 Update meshtastic/device-ui digest to 37ad715 (#9403)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-24 05:18:48 -06:00
renovate[bot]
04d2dd3b1c Update GxEPD2 to v1.6.6 (#9412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-24 05:16:47 -06:00
Justin E. Mann
6b88d37b73 To fix the gps power rail issue on RAK 19007 when RAK12023+RAK12035 is installed (#9409)
* asked claude to fix the gps power rail issue when the io slot is in use.. this fixes the gps when both the RAK12500 GPS module and the RAK12035 soil sensor modules are being used.

* remove do { ... } while(0) from RESTORE_3V3_POWER() Macro

* remove some comments

* cleaner macro

* removed more excessive comments

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-24 05:16:36 -06:00
Ben Meadors
d407ec1975 Merge remote-tracking branch 'origin/master' into develop 2026-01-24 04:47:24 -06:00
Jonathan Bennett
6d6a0734b0 Add pin sense to wake M6 on Solar Charge (#9416) 2026-01-23 15:37:16 -06:00
Mattijs
0157a769c3 Make BLE TX power configurable for nRF52 variants (#9232)
* Make BLE TX power configurable for nRF52 variants

* Include BLE TX power setting in T114 variant.h as tested

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-23 11:26:01 -06:00
Till Maas
73932dd1c3 device-install: Consistently use write-flash (#8868)
* flash scripts: Unify indentation

* flash scripts: Support esptool v4 and v5

esptool v5 supports commands with dashes and deprecates commands with
underscores. Prior versions only support commands with underscores.
2026-01-23 06:05:29 -06:00
HarukiToreda
bc2abf3db4 BaseUI: Bubbles for messages (#9365)
* Message Bubbles

* Angled edges

* Proper indent for messages inside the bubble

* Fix message header line width

* Correctly calculate text width for the header and shrink Channel Name is on OLED

---------

Co-authored-by: Jason P <applewiz@mac.com>
2026-01-22 17:00:21 -06:00
github-actions[bot]
073eb2c672 Automated version bumps (#9402)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-01-22 16:19:35 -06:00
Jorropo
4744010295 run trunk fmt -a (#9400)
* run trunk fmt -a

* fix bracket bug

This was introduced by @tedwardd and @thebentern in 021106dfe5.

See this diff:
         else
+            checkConfigPort = false;
             printf("Using config file %d\n", TCPPort);
2026-01-22 15:46:37 -06:00
Chloe Bethel
d8d02cd6ec Implement setting TX_GAIN_LORA for portduino (#8501)
* Implement setting TX_GAIN_LORA for portduino

* use std::size instead of sizeof

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-22 07:08:43 -06:00
renovate[bot]
3e3299f549 Update meshtastic/device-ui digest to 613c095 (#9383)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 09:07:23 +11:00
github-actions[bot]
eefc08087d Update protobufs (#9371)
Co-authored-by: jp-bennett <5630967+jp-bennett@users.noreply.github.com>
2026-01-20 10:29:11 -06:00
Andrew Yong
fb6d199d36 feat: Add Russell, a board designed to go Up! on a balloon (#9079)
Hardware repository: https://github.com/Meshtastic-Malaysia/russell
- Designed to mount on an ER34615/IFR32700 cell
- RAK3172 STM32WLE5CCU6 MCU + integrated SX1262 LoRa
- CDtop CD-PA1010D GPS
- Bosch Sensortec BME280 sensor
- Consonance CN3158 LiFePO4 solar charger

Signed-off-by: Andrew Yong <me@ndoo.sg>
2026-01-20 06:38:04 -06:00
Ben Meadors
fb3bf783dd Implement graduated scaling for NodeInfo send timeout based on active mesh size (#9364)
* Implement graduated scaling for NodeInfo send timeout based on active mesh size

* Shorter timeout still needed for pubkey unkown and ad-hoc send

* Update src/modules/NodeInfoModule.cpp

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-19 20:00:41 -06:00
Ben Meadors
fc268d43d0 Add Meshtastic exclusion flags for webserver and paxcounter in platformio.ini 2026-01-19 16:57:21 -06:00
Jonathan Bennett
c38aff7e52 Add interrupt for external charge detection (#9332)
Tested on Thinknode m4, m6, and T1000-e
2026-01-19 15:39:24 -06:00
Ben Meadors
7d4600f8c2 Merge branch 'master' into develop 2026-01-19 12:13:52 -06:00
Ben Meadors
ff50ba4002 Remove bsec from OG ESP32 to fix DRAM overflow 2026-01-19 12:12:14 -06:00
github-actions[bot]
5c401b8e34 Update protobufs (#9362)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-01-19 12:11:13 -06:00
Ben Meadors
c96ebf15fd Merge remote-tracking branch 'origin/master' into develop 2026-01-19 07:57:36 -06:00
github-actions[bot]
3e4239daf8 Upgrade trunk (#9330)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-01-19 07:22:27 -06:00
Ben Meadors
ab97c0126f Merge pull request #9355 from meshtastic/fix-bme
Untangle some BME680 ifdef spaghetti
2026-01-19 07:21:37 -06:00
github-actions[bot]
d34d694731 Update protobufs (#9360)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-01-19 06:21:01 -06:00
Jonathan Bennett
e545897d4e Untangle some BME680 ifdef spaghetti 2026-01-18 21:28:20 -06:00
renovate[bot]
caa6ec0e8a Update meshtastic/device-ui digest to 3480b73 (#9353)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 14:28:05 +11:00
Jason P
49accefd8b Don't Mute DMs just because we mute a channel (#9348)
* Don't Mute DMs just because we mute a channel

* Updated code to consolidate muting
2026-01-18 16:39:23 -05:00
Jason P
02f24b9015 Improve BaseUI Preset Change Flow (#9343)
* Reset Channel Number to 0 on Preset Change

* Add Channel Picker to LoRa Options

* Change Channel to Frequency Slot

* Catch comparison issue

* Reset override_frequency to ensure we correctly move to new Radio Preset

* CoPilot Suggestions
2026-01-18 16:38:46 -05:00
Catalin Patulea
33ae3777a3 toradio, fromradio OPTIONS handler: fix sending proper HTTP response. (#9322)
Before this (missing response):
$ curl -v -X OPTIONS http://meshtastic.local/api/v1/fromradio
* Host meshtastic.local:80 was resolved.
* IPv6: (none)
* IPv4: 192.168.0.19
*   Trying 192.168.0.19:80...
* Connected to meshtastic.local (192.168.0.19) port 80
* using HTTP/1.x
> OPTIONS /api/v1/fromradio HTTP/1.1
> Host: meshtastic.local
> User-Agent: curl/8.14.1
> Accept: */*
>
* Request completely sent off
* Empty reply from server
* shutting down connection #0
curl: (52) Empty reply from server

After this (proper HTTP 204 response):
$ curl -v -X OPTIONS http://meshtastic.local/api/v1/fromradio
* Host meshtastic.local:80 was resolved.
* IPv6: (none)
* IPv4: 192.168.0.19
*   Trying 192.168.0.19:80...
* Connected to meshtastic.local (192.168.0.19) port 80
* using HTTP/1.x
> OPTIONS /api/v1/fromradio HTTP/1.1
> Host: meshtastic.local
> User-Agent: curl/8.14.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 204 OK
< Content-Type: application/x-protobuf
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET
< X-Protobuf-Schema: https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto
<
* Connection #0 to host meshtastic.local left intact

This is related to https://github.com/meshtastic/firmware/issues/5385.

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-18 07:41:24 -06:00
Ted W.
021106dfe5 Add support for setting API port from the config file (#8435)
* Add support for setting API port from the config file

* Update PortduinoGlue.cpp

Fix typo in var identifier

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-17 15:23:16 -06:00
Jonathan Bennett
afbd9e2180 Filter BLE updates that don't change pairing status (#9333) 2026-01-16 13:52:04 -06:00
Tom Fifield
91dd39a651 Add sqlite depdendency (Cherry-picks from sfpp) (#9328)
* Add sqlite to build requires

* Add missed comma

* Add sqlite dev to more dockerfiles

* Alpine docker fix

* Add sqlite to build requires

* Add sqlite depdendency (Cherry-picks from sfpp)

Store and Forward Plus Plus requires sqlite to work.

This PR cherry picks the commits that added the dependency so that
this can be added, and reduce the amount of effort to review sfpp.

Authored-By: @jp-bennett

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2026-01-15 17:18:02 -06:00
Ben Meadors
64116cd0d3 Meshtastic OTA (moar) (#9327)
* Initial commit of combined BLE and WiFi OTA

* Incorporate ota_hash in AdminMessage protobuf

* OTA protobuf changes

* Trunk fmt

* Partition header check for OTA type

* Guards

* Guards

* Derp

* Missed one

---------

Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
2026-01-15 14:36:36 -06:00
Ben Meadors
d493f5f171 Merge branch 'master' into develop 2026-01-15 08:26:09 -06:00
Ben Meadors
c8f0295a9c Cleanup 2026-01-15 08:25:38 -06:00
Ben Meadors
3911d5fe15 Fix build with high / low i2c address for OLED 2026-01-15 07:54:33 -06:00
Ben Meadors
59bdb9b097 Merge remote-tracking branch 'origin/develop' 2026-01-15 06:49:05 -06:00
Ben Meadors
b4157bd9bb Heltec V4 TFT metadata (#9325)
* Upgrade trunk (#9323)

Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>

* ICM20948 IMU sleep (#9324)

* Add v4-tft metadata

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2026-01-15 06:48:41 -06:00
Austin
7e4e772113 Add EByte EoRa-Hub (#9169) 2026-01-15 06:24:10 -06:00
HarukiToreda
82735ca04e ICM20948 IMU sleep (#9324) 2026-01-15 06:23:40 -06:00
github-actions[bot]
e8fbdb4d84 Upgrade trunk (#9323)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-01-15 06:21:03 -06:00
Ben Meadors
a69e439dc1 Merge branch 'develop' 2026-01-15 06:19:49 -06:00
Ben Meadors
360579926c Trunk fmt 2026-01-15 06:19:18 -06:00
Ben Meadors
ff8316f895 Merge branch 'master' into develop 2026-01-15 06:18:43 -06:00
Jason P
6ee52ca7fa Node Actions Menu Overhaul (#9287)
* Start overhaul and clean up of the Node Actions menu

* Wired up commands - still a lot of work and testing

* Remove old favorites menu

* Remove addFavoritesMenu

* CoPilot to the rescue, wired up some function in both directions

* Clean up CoPilot actions

* Cross out Mute or Ignored in lists, add Save to NodeDB on changes

* Improve strikethrough for columns

* Correct menu wording and adjust vertical divider on Node List

* Code cleanup

* Testing unveiled some issues - fixed with these changes
2026-01-15 16:22:55 +11:00
Thomas Göttgens
233e6acc85 Preliminary Thinknode M4 Support (#8754)
* Preliminary Thinknode M4 Support

* oops

* Fix RF switch TX configuration

* trunk'd

* GPS fix for M4

* Battery handling and LED for M4

* Trunk

* Drop debug warnings

* Make Red LED notification

* Merge cleanup

* Make white LEDs flash during charge

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2026-01-14 21:36:53 -06:00
Lewis He
5f63f91cbc Added I2C scanner a check for the QMC6310N. (#9305)
* Added support for the new SSD1306 control panel.

* Added QMC6310N inspection to I2C scanner

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-14 20:54:57 -06:00
Ben Meadors
c0afe92a7f Meshtastic unified OTA (#9231)
* Initial commit of combined BLE and WiFi OTA

* Incorporate ota_hash in AdminMessage protobuf

* OTA protobuf changes

* Trunk fmt

---------

Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
2026-01-14 20:54:31 -06:00
Mike Robbins
a6a80b067f Recover long_name, short_name from our own NodeDB entry if device.proto is unreadable (#9248)
* Recover long_name, short_name from our own NodeDB entry if device.proto is unreadable

* NodeDB::loadFromDisk: restore long/short name with memcpy and explicit null termination

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-15 11:02:09 +11:00
oscgonfer
64e95554bb Small fix in register size for SHT4X (#9309) 2026-01-15 11:00:42 +11:00
renovate[bot]
6537eeab03 Update pschatzmann_arduino-audio-driver to v0.2.0 (#9272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-15 11:00:24 +11:00
brad112358
fad315e99d Fix rotary encoder long press (#9039) 2026-01-15 10:59:24 +11:00
renovate[bot]
2d4f1b6bfe Update Adafruit BMP280 to v3 (#9307)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-15 10:47:54 +11:00
oscgonfer
5a81403594 Move PMSA003I to separate class and update AQ telemetry (#7190) 2026-01-14 13:00:08 -06:00
Jonathan Bennett
5d7d1ae7a5 Adds Custom battery curve for thinknode m6 (#9313) 2026-01-14 11:40:35 -06:00
Manuel
940b3e236b fix GPS for T-Watch S3 plus (#9312)
* support T-Watch S3 Plus GPS

* HAS_GPS

* define BUTTON_PIN

* swap GPS pins, USB_MODE=1
2026-01-14 10:01:08 -06:00
vicliu
d1ae131502 T-Deck Pro: speed up eink force refresh (#9303) 2026-01-14 10:00:33 -06:00
Ben Meadors
552df4c88c Supress reboot banner in Reboot OTA 2026-01-14 07:06:40 -06:00
github-actions[bot]
cdbc8f48d4 Update protobufs (#9308)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-01-14 06:40:10 -06:00
Ben Meadors
919f214e8d Fix OTA partition name matching (#9302) 2026-01-14 06:33:01 -06:00
github-actions[bot]
89a83d00fa Upgrade trunk (#9306)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-01-14 06:26:31 -06:00
renovate[bot]
5610d4809c Update meshtastic/device-ui digest to 5a870c6 (#9301)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-13 15:59:09 -06:00
github-actions[bot]
dae4061b06 Update protobufs (#9299)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-01-13 05:58:12 -06:00
Mike Robbins
e99853f660 SafeFile: use atomic rename-with-overwrite, rather than non-atomic delete-then-rename (#9296) 2026-01-13 05:57:04 -06:00
github-actions[bot]
3640e35a8b Upgrade trunk (#9297)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-01-13 05:50:40 -06:00
Ben Meadors
782ffdc5cd Merge branch 'develop' 2026-01-13 05:48:19 -06:00
Ben Meadors
6f36f39da9 Fix up T-Beam 1W HW_MODEL 2026-01-13 05:48:14 -06:00
Ben Meadors
ded4f57cb7 Partition name in manifest script (#9294)
* Fix up T-Beam 1W HW_MODEL

* Add part_name for bin files

* app0
2026-01-13 05:47:08 -06:00
HarukiToreda
3a0f3520d1 BaseUI: Autosave Messages (#9269)
* Autosave Messages

* fix

* Add logging, code cleanup, and add save on delete.

* We already save as part of delete messages, no need to do it again

* fix spelling errors

* Updating comment

---------

Co-authored-by: Jason P <applewiz@mac.com>
2026-01-12 19:40:44 -06:00
405 changed files with 7876 additions and 2639 deletions

View File

@@ -20,7 +20,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
RUN apt-get update && apt-get install --no-install-recommends -y \
cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \
libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \
libusb-1.0-0-dev libssl-dev pkg-config && \
libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev && \
apt-get clean && rm -rf /var/lib/apt/lists/* && \
pip install --no-cache-dir -U \
platformio==6.1.16 \

1
.envrc Normal file
View File

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

View File

@@ -91,8 +91,8 @@ jobs:
if [[ -f "$manifest" ]]; then
echo "Updating $manifest with $OTA_FILE (md5: $OTA_MD5, size: $OTA_SIZE)"
# Add OTA entry to files array if not already present
jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" \
'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes}] else . end' \
jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" --arg part "app1" \
'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes, "part_name": $part}] else . end' \
"$manifest" > "${manifest}.tmp" && mv "${manifest}.tmp" "$manifest"
fi
done

View File

@@ -0,0 +1,213 @@
name: Issue Triage (Models)
on:
issues:
types: [opened]
permissions:
issues: write
models: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
triage:
if: ${{ github.repository == 'meshtastic/firmware' && github.event.issue.user.type != 'Bot' }}
runs-on: ubuntu-latest
steps:
# ─────────────────────────────────────────────────────────────────────────
# Step 1: Quality check (spam/AI-slop detection) - runs first, exits early if spam
# ─────────────────────────────────────────────────────────────────────────
- name: Detect spam or low-quality content
uses: actions/ai-inference@v2
id: quality
continue-on-error: true
with:
max-tokens: 20
prompt: |
Is this GitHub issue spam, AI-generated slop, or low quality?
Title: ${{ github.event.issue.title }}
Body: ${{ github.event.issue.body }}
Respond with exactly one of: spam, ai-generated, needs-review, ok
system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop.
model: openai/gpt-4o-mini
- name: Apply quality label if needed
if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
uses: actions/github-script@v8
env:
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
with:
script: |
const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase();
const labelMeta = {
'spam': { color: 'd73a4a', description: 'Possible spam' },
'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' },
'needs-review': { color: 'f9d0c4', description: 'Needs human review' },
};
const meta = labelMeta[label];
if (!meta) return;
// Ensure label exists
try {
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
} catch (e) {
if (e.status !== 404) throw e;
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
}
// Apply label
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] });
// Set output to skip remaining steps
core.setOutput('is_spam', 'true');
# ─────────────────────────────────────────────────────────────────────────
# Step 2: Duplicate detection - only if not spam
# ─────────────────────────────────────────────────────────────────────────
- name: Detect duplicate issues
if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
uses: pelikhan/action-genai-issue-dedup@bdb3b5d9451c1090ffcdf123d7447a5e7c7a2528 # v0.0.19
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# ─────────────────────────────────────────────────────────────────────────
# Step 3: Completeness check + auto-labeling (combined into one AI call)
# ─────────────────────────────────────────────────────────────────────────
- name: Determine if completeness check should be skipped
if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
uses: actions/github-script@v8
id: check-skip
with:
script: |
const title = (context.payload.issue.title || '').toLowerCase();
const labels = (context.payload.issue.labels || []).map(label => label.name);
const hasFeatureRequest = title.includes('feature request');
const hasEnhancement = labels.includes('enhancement');
const shouldSkip = hasFeatureRequest && hasEnhancement;
core.setOutput('should_skip', shouldSkip ? 'true' : 'false');
- name: Analyze issue completeness and determine labels
if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true'
uses: actions/ai-inference@v2
id: analysis
continue-on-error: true
with:
prompt: |
Analyze this GitHub issue for completeness and determine if it needs labels.
IMPORTANT: Distinguish between:
- Device/firmware bugs (crashes, reboots, lockups, radio/GPS/display/power issues) - these need device logs
- Build/release/packaging issues (missing files, CI failures, download problems) - these do NOT need device logs
- Documentation or website issues - these do NOT need device logs
If this is a device/firmware bug, request device logs and explain how to get them:
Web Flasher logs:
- Go to https://flasher.meshtastic.org
- Connect the device via USB and click Connect
- Open the device console/log output, reproduce the problem, then copy/download and attach/paste the logs
Meshtastic CLI logs:
- Run: meshtastic --port <serial-port> --noproto
- Reproduce the problem, then copy/paste the terminal output
Also request key context if missing: device model/variant, firmware version, region, steps to reproduce, expected vs actual.
Respond ONLY with valid JSON (no markdown, no code fences):
{"complete": true, "comment": "", "label": "none"}
OR
{"complete": false, "comment": "Your helpful comment", "label": "needs-logs"}
Use "needs-logs" ONLY if this is a device/firmware bug AND no logs are attached.
Use "needs-info" if basic info like firmware version or steps to reproduce are missing.
Use "none" if the issue is complete, is a feature request, or is a build/CI/packaging issue.
Title: ${{ github.event.issue.title }}
Body: ${{ github.event.issue.body }}
system-prompt: You are a helpful assistant that triages GitHub issues. Be conservative with labels. Only request device logs for actual device/firmware bugs, not for build/release/CI issues.
model: openai/gpt-4o-mini
- name: Process analysis result
if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != ''
uses: actions/github-script@v8
id: process
env:
AI_RESPONSE: ${{ steps.analysis.outputs.response }}
with:
script: |
let raw = (process.env.AI_RESPONSE || '').trim();
// Strip markdown code fences if present
raw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
let complete = true;
let comment = '';
let label = 'none';
try {
const parsed = JSON.parse(raw);
complete = !!parsed.complete;
comment = (parsed.comment ?? '').toString().trim();
label = (parsed.label ?? 'none').toString().trim().toLowerCase();
} catch {
// If JSON parse fails, log warning and don't comment (avoid posting raw JSON)
console.log('Failed to parse AI response as JSON:', raw);
complete = true;
comment = '';
label = 'none';
}
// Validate label
const allowedLabels = new Set(['needs-logs', 'needs-info', 'none']);
if (!allowedLabels.has(label)) label = 'none';
// Only comment if we have a valid parsed comment (not raw JSON)
const shouldComment = !complete && comment.length > 0 && !comment.startsWith('{');
core.setOutput('should_comment', shouldComment ? 'true' : 'false');
core.setOutput('comment_body', comment);
core.setOutput('label', label);
- name: Apply triage label
if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none'
uses: actions/github-script@v8
env:
LABEL_NAME: ${{ steps.process.outputs.label }}
with:
script: |
const label = process.env.LABEL_NAME;
const labelMeta = {
'needs-logs': { color: 'cfd3d7', description: 'Device logs requested for triage' },
'needs-info': { color: 'f9d0c4', description: 'More information requested for triage' },
};
const meta = labelMeta[label];
if (!meta) return;
// Ensure label exists
try {
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
} catch (e) {
if (e.status !== 404) throw e;
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
}
// Apply label
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] });
- name: Comment on issue
if: steps.process.outputs.should_comment == 'true'
uses: actions/github-script@v8
env:
COMMENT_BODY: ${{ steps.process.outputs.comment_body }}
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
body: process.env.COMMENT_BODY
});

139
.github/workflows/models_pr_triage.yml vendored Normal file
View File

@@ -0,0 +1,139 @@
name: PR Triage (Models)
on:
pull_request_target:
types: [opened]
permissions:
pull-requests: write
issues: write
models: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
triage:
if: ${{ github.repository == 'meshtastic/firmware' && github.event.pull_request.user.type != 'Bot' }}
runs-on: ubuntu-latest
steps:
# ─────────────────────────────────────────────────────────────────────────
# Step 1: Check if PR already has automation/type labels (skip if so)
# ─────────────────────────────────────────────────────────────────────────
- name: Check existing labels
uses: actions/github-script@v8
id: check-labels
with:
script: |
const skipLabels = new Set(['automation']);
const typeLabels = new Set(['bugfix', 'hardware-support', 'enhancement', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']);
const prLabels = context.payload.pull_request.labels.map(l => l.name);
const shouldSkipAll = prLabels.some(l => skipLabels.has(l));
const hasTypeLabel = prLabels.some(l => typeLabels.has(l));
core.setOutput('skip_all', shouldSkipAll ? 'true' : 'false');
core.setOutput('has_type_label', hasTypeLabel ? 'true' : 'false');
# ─────────────────────────────────────────────────────────────────────────
# Step 2: Quality check (spam/AI-slop detection)
# ─────────────────────────────────────────────────────────────────────────
- name: Detect spam or low-quality content
if: steps.check-labels.outputs.skip_all != 'true'
uses: actions/ai-inference@v2
id: quality
continue-on-error: true
with:
max-tokens: 20
prompt: |
Is this GitHub pull request spam, AI-generated slop, or low quality?
Title: ${{ github.event.pull_request.title }}
Body: ${{ github.event.pull_request.body }}
Respond with exactly one of: spam, ai-generated, needs-review, ok
system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop.
model: openai/gpt-4o-mini
- name: Apply quality label if needed
if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
uses: actions/github-script@v8
id: quality-label
env:
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
with:
script: |
const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase();
const labelMeta = {
'spam': { color: 'd73a4a', description: 'Possible spam' },
'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' },
'needs-review': { color: 'f9d0c4', description: 'Needs human review' },
};
const meta = labelMeta[label];
if (!meta) return;
// Ensure label exists
try {
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
} catch (e) {
if (e.status !== 404) throw e;
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
}
// Apply label
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] });
core.setOutput('is_spam', 'true');
# ─────────────────────────────────────────────────────────────────────────
# Step 3: Auto-label PR type (bugfix/hardware-support/enhancement)
# Only skip for spam/ai-generated; still classify needs-review PRs
# ─────────────────────────────────────────────────────────────────────────
- name: Classify PR for labeling
if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.quality.outputs.response != 'spam' && steps.quality.outputs.response != 'ai-generated'
uses: actions/ai-inference@v2
id: classify
continue-on-error: true
with:
max-tokens: 30
prompt: |
Classify this pull request into exactly one category.
Return exactly one of: bugfix, hardware-support, enhancement
Use bugfix if it fixes a bug, crash, or incorrect behavior.
Use hardware-support if it adds or improves support for a specific hardware device/variant.
Use enhancement if it adds a new feature, improves performance, or refactors code.
Title: ${{ github.event.pull_request.title }}
Body: ${{ github.event.pull_request.body }}
system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label.
model: openai/gpt-4o-mini
- name: Apply type label
if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != ''
uses: actions/github-script@v8
env:
TYPE_LABEL: ${{ steps.classify.outputs.response }}
with:
script: |
const label = (process.env.TYPE_LABEL || '').trim().toLowerCase();
const labelMeta = {
'bugfix': { color: 'd73a4a', description: 'Bug fix' },
'hardware-support': { color: '0e8a16', description: 'Hardware support addition or improvement' },
'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' },
};
const meta = labelMeta[label];
if (!meta) return;
// Ensure label exists
try {
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
} catch (e) {
if (e.status !== 404) throw e;
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
}
// Apply label
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] });

3
.gitignore vendored
View File

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

View File

@@ -9,14 +9,14 @@ plugins:
lint:
enabled:
- checkov@3.2.497
- renovate@42.78.2
- prettier@3.7.4
- trufflehog@3.92.4
- yamllint@1.37.1
- bandit@1.9.2
- renovate@42.84.2
- prettier@3.8.0
- trufflehog@3.92.5
- yamllint@1.38.0
- bandit@1.9.3
- trivy@0.68.2
- taplo@0.10.0
- ruff@0.14.11
- ruff@0.14.13
- isort@7.0.0
- markdownlint@0.47.0
- oxipng@10.0.0
@@ -26,7 +26,7 @@ lint:
- hadolint@2.14.0
- shfmt@3.6.0
- shellcheck@0.11.0
- black@25.12.0
- black@26.1.0
- git-diff-check
- gitleaks@8.30.0
- clang-format@16.0.3

View File

@@ -14,7 +14,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
curl wget g++ zip git ca-certificates pkg-config \
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir -U platformio \
&& mkdir /tmp/firmware

View File

@@ -11,7 +11,7 @@ RUN apk --no-cache add \
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
libgpiod-dev yaml-cpp-dev bluez-dev \
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
libx11-dev libinput-dev libxkbcommon-dev \
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev \
&& rm -rf /var/cache/apk/* \
&& pip install --no-cache-dir -U platformio \
&& mkdir /tmp/firmware

View File

@@ -0,0 +1,23 @@
Lora:
Module: sx1262
CS: 0
IRQ: 6
Reset: 1
Busy: 4
RXen: 2
DIO2_AS_RF_SWITCH: true
spidev: ch341
USB_PID: 0x5512
USB_VID: 0x1A86
DIO3_TCXO_VOLTAGE: true
# USB_Serialnum: 12345678
SX126X_MAX_POWER: 22
# Reduce output power to improve EMI
NUM_PA_POINTS: 22
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
# Note: This module integrates an additional PA to achieve higher output power.
# The 'power' parameter here does not represent the actual RF output.
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (122 dBm).
# Each array element corresponds to the additional gain when that input level is set,
# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index].
# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information.

View File

@@ -1,15 +0,0 @@
Lora:
Module: sx1262
CS: 0
IRQ: 6
Reset: 1
Busy: 4
RXen: 2
DIO2_AS_RF_SWITCH: true
spidev: ch341
USB_PID: 0x5512
USB_VID: 0x1A86
DIO3_TCXO_VOLTAGE: true
# USB_Serialnum: 12345678
SX126X_MAX_POWER: 30
# Reduce output power to improve EMI

View File

@@ -0,0 +1,23 @@
Lora:
Module: sx1268
CS: 0
IRQ: 6
Reset: 1
Busy: 4
RXen: 2
DIO2_AS_RF_SWITCH: true
spidev: ch341
USB_PID: 0x5512
USB_VID: 0x1A86
DIO3_TCXO_VOLTAGE: true
# USB_Serialnum: 12345678
SX126X_MAX_POWER: 22
# Reduce output power to improve EMI
NUM_PA_POINTS: 22
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
# Note: This module integrates an additional PA to achieve higher output power.
# The 'power' parameter here does not represent the actual RF output.
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (122 dBm).
# Each array element corresponds to the additional gain when that input level is set,
# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index].
# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information.

View File

@@ -1,15 +0,0 @@
Lora:
Module: sx1268
CS: 0
IRQ: 6
Reset: 1
Busy: 4
RXen: 2
DIO2_AS_RF_SWITCH: true
spidev: ch341
USB_PID: 0x5512
USB_VID: 0x1A86
DIO3_TCXO_VOLTAGE: true
# USB_Serialnum: 12345678
SX126X_MAX_POWER: 30
# Reduce output power to improve EMI

View File

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

View File

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

View File

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

View File

@@ -60,6 +60,14 @@ def manifest_gather(source, target, env):
board_platform = env.BoardConfig().get("platform")
board_mcu = env.BoardConfig().get("build.mcu").lower()
needs_ota_suffix = board_platform == "nordicnrf52"
# Mapping of bin files to their target partition names
# Maps the filename pattern to the partition name where it should be flashed
partition_map = {
f"{progname}.bin": "app0", # primary application slot (app0 / OTA_0)
lfsbin: "spiffs", # filesystem image flashed to spiffs
}
check_paths = [
progname,
f"{progname}.elf",
@@ -85,6 +93,9 @@ def manifest_gather(source, target, env):
"md5": f.get_content_hash(), # Returns MD5 hash
"bytes": f.get_size() # Returns file size in bytes
}
# Add part_name if this file represents a partition that should be flashed
if p in partition_map:
d["part_name"] = partition_map[p]
out.append(d)
print(d)
manifest_write(out, env)

View File

@@ -0,0 +1,38 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default.csv",
"memory_type": "qio_qspi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=0",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi"],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "CDEBYTE_EoRa-Hub",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 4194304,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.cdebyte.com/products/EoRa-HUB-900TB",
"vendor": "CDEBYTE"
}

53
boards/ThinkNode-M4.json Normal file
View File

@@ -0,0 +1,53 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_ELECROW_M4 -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x4405"],
["0x239A", "0x0029"],
["0x239A", "0x002A"]
],
"usb_product": "elecrow_thinknode_m4",
"mcu": "nrf52840",
"variant": "ELECROW-ThinkNode-M4",
"variants_dir": "variants",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"onboard_tools": ["jlink"],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "ELECROW ThinkNode m4",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://www.elecrow.com/thinknode-m4-power-bank-lora-device-with-meshtastic-lora-tracker-function-powered-by-nrf52840.html",
"vendor": "ELECROW"
}

50
boards/minimesh_lite.json Normal file
View File

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

View File

@@ -9,7 +9,7 @@
"-DBOARD_HAS_PSRAM",
"-DT_WATCH_S3",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],

6
debian/changelog vendored
View File

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

3
debian/control vendored
View File

@@ -25,7 +25,8 @@ Build-Depends: debhelper-compat (= 13),
liborcania-dev,
libx11-dev,
libinput-dev,
libxkbcommon-x11-dev
libxkbcommon-x11-dev,
libsqlite3-dev
Standards-Version: 4.6.2
Homepage: https://github.com/meshtastic/firmware
Rules-Requires-Root: no

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

@@ -39,6 +39,7 @@ BuildRequires: pkgconfig(bluez)
BuildRequires: pkgconfig(libusb-1.0)
BuildRequires: libi2c-devel
BuildRequires: pkgconfig(libuv)
BuildRequires: pkgconfig(sqlite3)
# Web components:
BuildRequires: pkgconfig(openssl)
BuildRequires: pkgconfig(liborcania)

View File

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

View File

@@ -56,6 +56,7 @@ build_flags = -Wno-missing-field-initializers
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
-DMESHTASTIC_EXCLUDE_POWERMON=1
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
-DLED_BUILTIN=-1
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
#-D OLED_PL=1
#-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs
@@ -119,7 +120,7 @@ lib_deps =
[device-ui_base]
lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/14dc991a4b57066e0a83599be1a87ff08e5e5308.zip
https://github.com/meshtastic/device-ui/archive/63967a4a557d33d56fc5746f9128200dde2d88c5.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]
@@ -129,7 +130,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor
adafruit/Adafruit Unified Sensor@1.1.15
# renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library
adafruit/Adafruit BMP280 Library@2.6.8
adafruit/Adafruit BMP280 Library@3.0.0
# renovate: datasource=custom.pio depName=Adafruit BMP085 packageName=adafruit/library/Adafruit BMP085 Library
adafruit/Adafruit BMP085 Library@1.2.4
# renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library
@@ -142,8 +143,6 @@ lib_deps =
adafruit/Adafruit INA260 Library@1.5.3
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
adafruit/Adafruit INA219@1.2.3
# renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor
adafruit/Adafruit PM25 AQI Sensor@2.0.0
# renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050
adafruit/Adafruit MPU6050@2.2.6
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH
@@ -214,3 +213,30 @@ 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 =
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
adafruit/Adafruit BMP3XX Library@2.1.6
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
adafruit/Adafruit MAX1704X@1.0.3
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
adafruit/Adafruit SHTC3 Library@1.0.2
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
adafruit/Adafruit LPS2X@2.0.6
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library
adafruit/Adafruit SHT31 Library@2.2.2
# renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library
adafruit/Adafruit VEML7700 Library@2.1.6
# renovate: datasource=custom.pio depName=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library
adafruit/Adafruit SHT4x Library@1.0.5
# renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
closedcube/ClosedCube OPT3001@1.1.2
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
sensirion/Sensirion Core@0.7.2
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0

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

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

View File

@@ -13,6 +13,11 @@
#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE)
#endif
// Default autosave interval 2 hours, override per device later with -DMESSAGE_AUTOSAVE_INTERVAL_SEC=300 (etc)
#ifndef MESSAGE_AUTOSAVE_INTERVAL_SEC
#define MESSAGE_AUTOSAVE_INTERVAL_SEC (2 * 60 * 60)
#endif
// Global message text pool and state
static char *g_messagePool = nullptr;
static size_t g_poolWritePos = 0;
@@ -102,6 +107,60 @@ void MessageStore::addLiveMessage(const StoredMessage &msg)
pushWithLimit(liveMessages, msg);
}
#if ENABLE_MESSAGE_PERSISTENCE
static bool g_messageStoreHasUnsavedChanges = false;
static uint32_t g_lastAutoSaveMs = 0; // last time we actually saved
static inline uint32_t autosaveIntervalMs()
{
uint32_t sec = (uint32_t)MESSAGE_AUTOSAVE_INTERVAL_SEC;
if (sec < 60)
sec = 60;
return sec * 1000UL;
}
static inline bool reachedMs(uint32_t now, uint32_t target)
{
return (int32_t)(now - target) >= 0;
}
// Mark new messages in RAM that need to be saved later
static inline void markMessageStoreUnsaved()
{
g_messageStoreHasUnsavedChanges = true;
if (g_lastAutoSaveMs == 0) {
g_lastAutoSaveMs = millis();
}
}
// Called periodically from the main loop in main.cpp
static inline void autosaveTick(MessageStore *store)
{
if (!store)
return;
uint32_t now = millis();
if (g_lastAutoSaveMs == 0) {
g_lastAutoSaveMs = now;
return;
}
if (!reachedMs(now, g_lastAutoSaveMs + autosaveIntervalMs()))
return;
// Autosave interval reached, only save if there are unsaved messages.
if (g_messageStoreHasUnsavedChanges) {
LOG_INFO("Autosaving MessageStore to flash");
store->saveToFlash();
} else {
LOG_INFO("Autosave skipped, no changes to save");
g_lastAutoSaveMs = now;
}
}
#endif
// Add from incoming/outgoing packet
const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
{
@@ -131,6 +190,11 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa
}
addLiveMessage(sm);
#if ENABLE_MESSAGE_PERSISTENCE
markMessageStoreUnsaved();
#endif
return liveMessages.back();
}
@@ -155,6 +219,10 @@ void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const st
sm.ackStatus = AckStatus::NONE;
addLiveMessage(sm);
#if ENABLE_MESSAGE_PERSISTENCE
markMessageStoreUnsaved();
#endif
}
#if ENABLE_MESSAGE_PERSISTENCE
@@ -239,6 +307,10 @@ void MessageStore::saveToFlash()
f.close();
#endif
// Reset autosave state after any save
g_messageStoreHasUnsavedChanges = false;
g_lastAutoSaveMs = millis();
}
void MessageStore::loadFromFlash()
@@ -270,6 +342,9 @@ void MessageStore::loadFromFlash()
f.close();
#endif
// Loading messages does not trigger an autosave
g_messageStoreHasUnsavedChanges = false;
g_lastAutoSaveMs = millis();
}
#else
@@ -290,6 +365,11 @@ void MessageStore::clearAllMessages()
f.write(&count, 1); // write "0 messages"
f.close();
#endif
#if ENABLE_MESSAGE_PERSISTENCE
g_messageStoreHasUnsavedChanges = false;
g_lastAutoSaveMs = millis();
#endif
}
// Internal helper: erase first or last message matching a predicate
@@ -421,6 +501,14 @@ uint16_t MessageStore::storeText(const char *src, size_t len)
return storeTextInPool(src, len);
}
#if ENABLE_MESSAGE_PERSISTENCE
void messageStoreAutosaveTick()
{
// Called from the main loop to check autosave timing
autosaveTick(&messageStore);
}
#endif
// Global definition
MessageStore messageStore("default");
#endif
#endif

View File

@@ -125,6 +125,11 @@ class MessageStore
std::string filename; // Flash filename for persistence
};
#if ENABLE_MESSAGE_PERSISTENCE
// Called periodically from main loop to trigger time based autosave
void messageStoreAutosaveTick();
#endif
// Global instance (defined in MessageStore.cpp)
extern MessageStore messageStore;

View File

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

View File

@@ -54,7 +54,7 @@ size_t SafeFile::write(const uint8_t *buffer, size_t size)
}
/**
* Atomically close the file (deleting any old versions) and readback the contents to confirm the hash matches
* Atomically close the file (overwriting any old version) and readback the contents to confirm the hash matches
*
* @return false for failure
*/
@@ -73,15 +73,7 @@ bool SafeFile::close()
if (!testReadback())
return false;
{ // Scope for lock
concurrency::LockGuard g(spiLock);
// brief window of risk here ;-)
if (fullAtomic && FSCom.exists(filename.c_str()) && !FSCom.remove(filename.c_str())) {
LOG_ERROR("Can't remove old pref file");
return false;
}
}
// Rename or overwrite (atomic operation)
String filenameTmp = filename;
filenameTmp += ".tmp";
if (!renameFile(filenameTmp.c_str(), filename.c_str())) {

View File

@@ -155,6 +155,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#endif
// Default system gain to 0 if not defined
#ifndef NUM_PA_POINTS
#define NUM_PA_POINTS 1
#endif
#ifndef TX_GAIN_LORA
#define TX_GAIN_LORA 0
#endif
@@ -172,11 +176,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// -----------------------------------------------------------------------------
// OLED & Input
// -----------------------------------------------------------------------------
#define SSD1306_ADDRESS_L 0x3C // Addr = 0
#define SSD1306_ADDRESS_H 0x3D // Addr = 1
#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK)
#define SSD1306_ADDRESS 0x3D
#define SSD1306_ADDRESS SSD1306_ADDRESS_H
#define USE_SH1106
#else
#define SSD1306_ADDRESS 0x3C
#endif
#define ST7567_ADDRESS 0x3F
@@ -205,7 +210,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define INA_ADDR_WAVESHARE_UPS 0x43
#define INA3221_ADDR 0x42
#define MAX1704X_ADDR 0x36
#define QMC6310_ADDR 0x1C
#define QMC6310U_ADDR 0x1C
#define QMI8658_ADDR 0x6B
#define QMC5883L_ADDR 0x0D
#define HMC5883L_ADDR 0x1E
@@ -214,7 +219,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define LPS22HB_ADDR_ALT 0x5D
#define SHT31_4x_ADDR 0x44
#define SHT31_4x_ADDR_ALT 0x45
#define PMSA0031_ADDR 0x12
#define PMSA003I_ADDR 0x12
#define QMA6100P_ADDR 0x12
#define AHT10_ADDR 0x38
#define RCWL9620_ADDR 0x57
@@ -269,9 +274,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define TCA9535_ADDR 0x20
#define TCA9555_ADDR 0x26
// used for display brightness control
#define STC8H1K28_ADDR 0x30
// -----------------------------------------------------------------------------
// Touchscreen
// -----------------------------------------------------------------------------
@@ -388,9 +390,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#ifndef HAS_RADIO
#define HAS_RADIO 0
#endif
#ifndef HAS_RTC
#define HAS_RTC 0
#endif
#ifndef HAS_CPU_SHUTDOWN
#define HAS_CPU_SHUTDOWN 0
#endif
@@ -426,12 +425,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define HAS_RGB_LED
#endif
#ifndef LED_STATE_OFF
#define LED_STATE_OFF 0
#endif
#ifndef LED_STATE_ON
#define LED_STATE_ON 1
#endif
#ifndef LED_STATE_OFF
#define LED_STATE_OFF (LED_STATE_ON ^ 1)
#endif
#ifndef ledOff
#define ledOff(pin) pinMode(pin, INPUT)
#endif
// default mapping of pins
#if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN)
@@ -447,18 +450,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#endif
#endif
// BME680 BSEC2 support detection
#if !defined(MESHTASTIC_BME680_BSEC2_SUPPORTED)
#if defined(RAK_4631) || defined(TBEAM_V10)
#define MESHTASTIC_BME680_BSEC2_SUPPORTED 1
#define MESHTASTIC_BME680_HEADER <bsec2.h>
#else
#define MESHTASTIC_BME680_BSEC2_SUPPORTED 0
#define MESHTASTIC_BME680_HEADER <Adafruit_BME680.h>
#endif // defined(RAK_4631)
#endif // !defined(MESHTASTIC_BME680_BSEC2_SUPPORTED)
// -----------------------------------------------------------------------------
// Global switches to turn off features for a minimized build
// -----------------------------------------------------------------------------
@@ -483,6 +474,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define MESHTASTIC_EXCLUDE_AUDIO 1
#define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1
#define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1
#define MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR 1
#define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 1
#define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1
#define MESHTASTIC_EXCLUDE_PAXCOUNTER 1

View File

@@ -43,7 +43,7 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
ScanI2C::FoundDevice ScanI2C::firstAQI() const
{
ScanI2C::DeviceType types[] = {PMSA0031, SCD4X};
ScanI2C::DeviceType types[] = {PMSA003I, SCD4X};
return firstOfOrNONE(2, types);
}

View File

@@ -35,11 +35,12 @@ class ScanI2C
SHT4X,
SHTC3,
LPS22HB,
QMC6310,
QMC6310U,
QMC6310N,
QMI8658,
QMC5883L,
HMC5883L,
PMSA0031,
PMSA003I,
QMA6100P,
MPU6050,
LIS3DH,
@@ -62,7 +63,6 @@ class ScanI2C
NAU7802,
FT6336U,
STK8BAXX,
STC8H1K28,
ICM20948,
SCD4X,
MAX30102,

View File

@@ -63,6 +63,10 @@ ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const
if (i2cBus->available()) {
r = i2cBus->read();
}
if (r == 0x80) {
LOG_INFO("QMC6310N found at address 0x%02X", addr.address);
return ScanI2C::DeviceType::QMC6310N;
}
r &= 0x0f;
if (r == 0x08 || r == 0x00) {
@@ -106,7 +110,7 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
if (i2cBus->available())
i2cBus->read();
}
LOG_DEBUG("Register value: 0x%x", value);
LOG_DEBUG("Register value from 0x%x: 0x%x", registerLocation.i2cAddress.address, value);
return value;
}
@@ -175,7 +179,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
type = NONE;
if (err == 0) {
switch (addr.address) {
case SSD1306_ADDRESS:
case SSD1306_ADDRESS_H:
case SSD1306_ADDRESS_L:
type = probeOLED(addr);
break;
@@ -238,8 +243,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
#endif
#ifdef HAS_LP5562
SCAN_SIMPLE_CASE(LP5562_ADDR, LP5562, "LP5562", (uint8_t)addr.address);
#else
SCAN_SIMPLE_CASE(STC8H1K28_ADDR, LP5562, "STC8H1K28", (uint8_t)addr.address);
#endif
case XPOWERS_AXP192_AXP2101_ADDRESS:
// Do we have the axp2101/192 or the TCA8418
@@ -384,11 +387,11 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
}
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2);
if (registerValue == 0x5449) {
if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
type = OPT3001;
logFoundDevice("OPT3001", (uint8_t)addr.address);
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2) != 0) { // unique SHT4x serial number
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) !=
0) { // unique SHT4x serial number (6 bytes inc. CRC)
type = SHT4X;
logFoundDevice("SHT4X", (uint8_t)addr.address);
} else {
@@ -414,7 +417,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
case LPS22HB_ADDR_ALT:
SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address)
SCAN_SIMPLE_CASE(QMC6310_ADDR, QMC6310, "QMC6310", (uint8_t)addr.address)
SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address)
case QMI8658_ADDR:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID
@@ -444,7 +447,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
#ifdef HAS_QMA6100P
SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address)
#else
SCAN_SIMPLE_CASE(PMSA0031_ADDR, PMSA0031, "PMSA0031", (uint8_t)addr.address)
SCAN_SIMPLE_CASE(PMSA003I_ADDR, PMSA003I, "PMSA003I", (uint8_t)addr.address)
#endif
case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2);

41
src/detect/reClockI2C.h Normal file
View File

@@ -0,0 +1,41 @@
#ifdef CAN_RECLOCK_I2C
#include "ScanI2CTwoWire.h"
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus)
{
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

@@ -896,18 +896,21 @@ void GPS::writePinEN(bool on)
void GPS::writePinStandby(bool standby)
{
#ifdef PIN_GPS_STANDBY // Specifically the standby pin for L76B, L76K and clones
// Determine the new value for the pin
// Normally: active HIGH for awake
#ifdef PIN_GPS_STANDBY_INVERTED
bool val = standby;
#else
bool val = !standby;
#endif
bool val;
if (standby)
val = GPS_STANDBY_ACTIVE;
else
val = !GPS_STANDBY_ACTIVE;
// Write and log
pinMode(PIN_GPS_STANDBY, OUTPUT);
digitalWrite(PIN_GPS_STANDBY, val);
// Enter backup mode on PA1010D; TODO: may be applicable to other MTK GPS too
if (IS_ONE_OF(gnssModel, GNSS_MODEL_MTK_PA1010D)) {
_serial_gps->write("$PMTK225,4*2F\r\n");
}
#ifdef GPS_DEBUG
LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW");
#endif

View File

@@ -16,6 +16,11 @@
#define GPS_EN_ACTIVE 1
#endif
// Allow defining the polarity of the STANDBY output. default is LOW for standby
#ifndef GPS_STANDBY_ACTIVE
#define GPS_STANDBY_ACTIVE LOW
#endif
static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL;
static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000;

View File

@@ -276,11 +276,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
@@ -397,7 +393,7 @@ uint32_t getValidTime(RTCQuality minQuality, bool local)
return (currentQuality >= minQuality) ? getTime(local) : 0;
}
time_t gm_mktime(struct tm *tm)
time_t gm_mktime(const struct tm *tm)
{
#if !MESHTASTIC_EXCLUDE_TZ
time_t result = 0;
@@ -413,8 +409,8 @@ time_t gm_mktime(struct tm *tm)
days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400);
// Now, within this tm->year, compute the days *before* this tm->month starts.
int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year
int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11
static const int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year
int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11
// If this is a leap year, and we're past February, add a day:
if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) {
@@ -435,6 +431,7 @@ time_t gm_mktime(struct tm *tm)
return result;
#else
return mktime(tm);
struct tm tmCopy = *tm;
return mktime(&tmCopy);
#endif
}

View File

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

View File

@@ -9,6 +9,15 @@
#include "GxEPD2Multi.h"
#endif
// Limit how often we push a full E-Ink refresh. T-Deck Pro needs faster updates for typing.
#ifndef EINK_FORCE_DISPLAY_THROTTLE_MS
#if defined(T_DECK_PRO)
#define EINK_FORCE_DISPLAY_THROTTLE_MS 200
#else
#define EINK_FORCE_DISPLAY_THROTTLE_MS 1000
#endif
#endif
/**
* An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation.
*
@@ -42,7 +51,7 @@ class EInkDisplay : public OLEDDisplay
*
* @return true if we did draw the screen
*/
virtual bool forceDisplay(uint32_t msecLimit = 1000);
virtual bool forceDisplay(uint32_t msecLimit = EINK_FORCE_DISPLAY_THROTTLE_MS);
/**
* Run any code needed to complete an update, after the physical refresh has completed.

View File

@@ -825,7 +825,7 @@ int32_t Screen::runOnce()
#endif
}
#endif
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0 && !suppressRebootBanner) {
showSimpleBanner("Rebooting...", 0);
}
@@ -1731,6 +1731,26 @@ int Screen::handleInputEvent(const InputEvent *event)
showFrame(FrameDirection::PREVIOUS);
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
showFrame(FrameDirection::NEXT);
} else if (event->inputEvent == INPUT_BROKER_FN_F1) {
this->ui->switchToFrame(0);
lastScreenTransition = millis();
setFastFramerate();
} else if (event->inputEvent == INPUT_BROKER_FN_F2) {
this->ui->switchToFrame(1);
lastScreenTransition = millis();
setFastFramerate();
} else if (event->inputEvent == INPUT_BROKER_FN_F3) {
this->ui->switchToFrame(2);
lastScreenTransition = millis();
setFastFramerate();
} else if (event->inputEvent == INPUT_BROKER_FN_F4) {
this->ui->switchToFrame(3);
lastScreenTransition = millis();
setFastFramerate();
} else if (event->inputEvent == INPUT_BROKER_FN_F5) {
this->ui->switchToFrame(4);
lastScreenTransition = millis();
setFastFramerate();
} else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
// Long press up button for fast frame switching
showPrevFrame();

View File

@@ -438,7 +438,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
if (currentResolution == ScreenResolution::UltraLow) {
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num);
} else {
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num);
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz (%d)", freqStr, config.lora.channel_num);
}
}
size_t len = strlen(frequencyslot);

View File

@@ -59,17 +59,18 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp
} // namespace
menuHandler::screenMenus menuHandler::menuQueue = menu_none;
uint32_t menuHandler::pickedNodeNum = 0;
bool test_enabled = false;
uint8_t test_count = 0;
void menuHandler::loraMenu()
{
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "LoRa Region"};
enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, lora_picker = 3 };
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 };
BannerOverlayOptions bannerOptions;
bannerOptions.message = "LoRa Actions";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 4;
bannerOptions.optionsCount = 5;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
// No action
@@ -77,6 +78,8 @@ void menuHandler::loraMenu()
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;
}
@@ -247,6 +250,69 @@ void menuHandler::DeviceRolePicker()
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::FrequencySlotPicker()
{
enum ReplyOptions : int { Back = -1 };
constexpr int MAX_CHANNEL_OPTIONS = 202;
static const char *optionsArray[MAX_CHANNEL_OPTIONS];
static int optionsEnumArray[MAX_CHANNEL_OPTIONS];
static char channelText[MAX_CHANNEL_OPTIONS - 1][12];
int options = 0;
optionsArray[options] = "Back";
optionsEnumArray[options++] = Back;
optionsArray[options] = "Slot 0 (Auto)";
optionsEnumArray[options++] = 0;
// Calculate number of channels (copied from RadioInterface::applyModemConfig())
meshtastic_Config_LoRaConfig &loraConfig = config.lora;
double bw = loraConfig.use_preset ? modemPresetToBwKHz(loraConfig.modem_preset, myRegion->wideLora)
: bwCodeToKHz(loraConfig.bandwidth);
uint32_t numChannels = 0;
if (myRegion) {
numChannels = (uint32_t)floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000.0)));
} else {
LOG_WARN("Region not set, cannot calculate number of channels");
return;
}
if (numChannels > (uint32_t)(MAX_CHANNEL_OPTIONS - 2))
numChannels = (uint32_t)(MAX_CHANNEL_OPTIONS - 2);
for (uint32_t ch = 1; ch <= numChannels; ch++) {
snprintf(channelText[ch - 1], sizeof(channelText[ch - 1]), "Slot %lu", (unsigned long)ch);
optionsArray[options] = channelText[ch - 1];
optionsEnumArray[options++] = (int)ch;
}
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Frequency Slot";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options;
// Start highlight on current channel if possible, otherwise on "1"
int initial = (int)config.lora.channel_num + 1;
if (initial < 2 || initial > (int)numChannels + 1)
initial = 1;
bannerOptions.InitialSelected = initial;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::lora_Menu;
screen->runNow();
return;
}
config.lora.channel_num = selected;
service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::RadioPresetPicker()
{
static const RadioPresetOption presetOptions[] = {
@@ -277,6 +343,8 @@ void menuHandler::RadioPresetPicker()
}
config.lora.modem_preset = option.value;
config.lora.channel_num = 0; // Reset to default channel for the preset
config.lora.override_frequency = 0; // Clear any custom frequency
service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
});
@@ -449,7 +517,7 @@ void menuHandler::clockMenu()
}
void menuHandler::messageResponseMenu()
{
enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, MuteChannel, Aloud, enumEnd };
enum optionsNumbers { Back = 0, ViewMode, DeleteMenu, ReplyMenu, MuteChannel, Aloud, enumEnd };
static const char *optionsArray[enumEnd];
static int optionsEnumArray[enumEnd];
@@ -479,7 +547,7 @@ void menuHandler::messageResponseMenu()
// Delete submenu
optionsArray[options] = "Delete";
optionsEnumArray[options++] = 900;
optionsEnumArray[options++] = DeleteMenu;
#ifdef HAS_I2S
optionsArray[options] = "Read Aloud";
@@ -520,34 +588,10 @@ void menuHandler::messageResponseMenu()
nodeDB->saveToDisk();
}
// Delete submenu
} else if (selected == 900) {
} else if (selected == DeleteMenu) {
menuHandler::menuQueue = menuHandler::delete_messages_menu;
screen->runNow();
// Delete oldest FIRST (only change)
} else if (selected == DeleteOldest) {
auto mode = graphics::MessageRenderer::getThreadMode();
int ch = graphics::MessageRenderer::getThreadChannel();
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
// Global oldest
messageStore.deleteOldestMessage();
} else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
// Oldest in current channel
messageStore.deleteOldestMessageInChannel(ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
// Oldest in current DM
messageStore.deleteOldestMessageWithPeer(peer);
}
// Delete all messages
} else if (selected == DeleteAll) {
messageStore.clearAllMessages();
graphics::MessageRenderer::clearThreadRegistries();
graphics::MessageRenderer::clearMessageCache();
#ifdef HAS_I2S
} else if (selected == Aloud) {
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
@@ -716,7 +760,6 @@ void menuHandler::deleteMessagesMenu()
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
messageStore.deleteOldestMessageWithPeer(peer);
}
return;
}
@@ -729,7 +772,6 @@ void menuHandler::deleteMessagesMenu()
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
messageStore.deleteAllMessagesWithPeer(peer);
}
return;
}
};
@@ -1239,20 +1281,13 @@ void menuHandler::positionBaseMenu()
void menuHandler::nodeListMenu()
{
enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd };
enum optionsNumbers { Back, NodePicker, TraceRoute, Verify, Reset, NodeNameLength, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back};
int options = 1;
optionsArray[options] = "Add Favorite";
optionsEnumArray[options++] = Favorite;
optionsArray[options] = "Trace Route";
optionsEnumArray[options++] = TraceRoute;
if (currentResolution != ScreenResolution::UltraLow) {
optionsArray[options] = "Key Verification";
optionsEnumArray[options++] = Verify;
}
optionsArray[options] = "Node Actions / Settings";
optionsEnumArray[options++] = NodePicker;
if (currentResolution != ScreenResolution::UltraLow) {
optionsArray[options] = "Show Long/Short Name";
@@ -1267,18 +1302,12 @@ void menuHandler::nodeListMenu()
bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Favorite) {
menuQueue = add_favorite;
screen->runNow();
} else if (selected == Verify) {
menuQueue = key_verification_init;
if (selected == NodePicker) {
menuQueue = NodePicker_menu;
screen->runNow();
} else if (selected == Reset) {
menuQueue = reset_node_db_menu;
screen->runNow();
} else if (selected == TraceRoute) {
menuQueue = trace_route_menu;
screen->runNow();
} else if (selected == NodeNameLength) {
menuHandler::menuQueue = menuHandler::node_name_length_menu;
screen->runNow();
@@ -1287,6 +1316,159 @@ void menuHandler::nodeListMenu()
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::NodePicker()
{
const char *NODE_PICKER_TITLE;
if (currentResolution == ScreenResolution::UltraLow) {
NODE_PICKER_TITLE = "Pick Node";
} else {
NODE_PICKER_TITLE = "Pick A Node";
}
screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void {
LOG_INFO("Nodenum: %u", nodenum);
// Store the selection so the Manage Node menu knows which node to operate on
menuHandler::pickedNodeNum = nodenum;
// Keep UI favorite context in sync (used elsewhere for some node-based actions)
graphics::UIRenderer::currentFavoriteNodeNum = nodenum;
menuQueue = Manage_Node_menu;
screen->runNow();
});
}
void menuHandler::ManageNodeMenu()
{
// If we don't have a node selected yet, go fast exit
auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
if (!node) {
return;
}
enum optionsNumbers { Back, Favorite, Mute, TraceRoute, KeyVerification, Ignore, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back};
int options = 1;
if (node->is_favorite) {
optionsArray[options] = "Unfavorite";
} else {
optionsArray[options] = "Favorite";
}
optionsEnumArray[options++] = Favorite;
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
if (isMuted) {
optionsArray[options] = "Unmute Notifications";
} else {
optionsArray[options] = "Mute Notifications";
}
optionsEnumArray[options++] = Mute;
optionsArray[options] = "Trace Route";
optionsEnumArray[options++] = TraceRoute;
optionsArray[options] = "Key Verification";
optionsEnumArray[options++] = KeyVerification;
if (node->is_ignored) {
optionsArray[options] = "Unignore Node";
} else {
optionsArray[options] = "Ignore Node";
}
optionsEnumArray[options++] = Ignore;
BannerOverlayOptions bannerOptions;
std::string title = "";
if (node->has_user && node->user.long_name && node->user.long_name[0]) {
title += sanitizeString(node->user.long_name).substr(0, 15);
} else {
char buf[20];
snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num);
title += buf;
}
bannerOptions.message = title.c_str();
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuQueue = node_base_menu;
screen->runNow();
return;
}
if (selected == Favorite) {
auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
if (!n) {
return;
}
if (n->is_favorite) {
LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum);
nodeDB->set_favorite(false, menuHandler::pickedNodeNum);
} else {
LOG_INFO("Adding node %08X to favorites", menuHandler::pickedNodeNum);
nodeDB->set_favorite(true, menuHandler::pickedNodeNum);
}
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
return;
}
if (selected == Mute) {
auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
if (!n) {
return;
}
if (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) {
n->bitfield &= ~NODEINFO_BITFIELD_IS_MUTED_MASK;
LOG_INFO("Unmuted node %08X", menuHandler::pickedNodeNum);
} else {
n->bitfield |= NODEINFO_BITFIELD_IS_MUTED_MASK;
LOG_INFO("Muted node %08X", menuHandler::pickedNodeNum);
}
nodeDB->notifyObservers(true);
nodeDB->saveToDisk();
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
return;
}
if (selected == TraceRoute) {
LOG_INFO("Starting traceroute to %08X", menuHandler::pickedNodeNum);
if (traceRouteModule) {
traceRouteModule->startTraceRoute(menuHandler::pickedNodeNum);
}
return;
}
if (selected == KeyVerification) {
LOG_INFO("Initiating key verification with %08X", menuHandler::pickedNodeNum);
if (keyVerificationModule) {
keyVerificationModule->sendInitialRequest(menuHandler::pickedNodeNum);
}
return;
}
if (selected == Ignore) {
auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
if (!n) {
return;
}
if (n->is_ignored) {
n->is_ignored = false;
LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum);
} else {
n->is_ignored = true;
LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum);
}
nodeDB->notifyObservers(true);
nodeDB->saveToDisk();
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
return;
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::nodeNameLengthMenu()
{
static const NodeNameOption nodeNameOptions[] = {
@@ -1315,6 +1497,7 @@ void menuHandler::nodeNameLengthMenu()
}
config.display.use_long_node_name = option.value;
saveUIConfig();
LOG_INFO("Setting names to %s", option.value ? "long" : "short");
});
@@ -1984,21 +2167,6 @@ void menuHandler::shutdownMenu()
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::addFavoriteMenu()
{
const char *NODE_PICKER_TITLE;
if (currentResolution == ScreenResolution::UltraLow) {
NODE_PICKER_TITLE = "Node Favorite";
} else {
NODE_PICKER_TITLE = "Node To Favorite";
}
screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void {
LOG_WARN("Nodenum: %u", nodenum);
nodeDB->set_favorite(true, nodenum);
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
});
}
void menuHandler::removeFavoriteMenu()
{
@@ -2273,7 +2441,8 @@ void menuHandler::FrameToggles_menu()
lora,
clock,
show_favorites,
show_telemetry,
show_env_telemetry,
show_aq_telemetry,
show_power,
enumEnd
};
@@ -2318,8 +2487,11 @@ void menuHandler::FrameToggles_menu()
optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites";
optionsEnumArray[options++] = show_favorites;
optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Telemetry" : "Show Telemetry";
optionsEnumArray[options++] = show_telemetry;
optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Env. Telemetry" : "Show Env. Telemetry";
optionsEnumArray[options++] = show_env_telemetry;
optionsArray[options] = moduleConfig.telemetry.air_quality_screen_enabled ? "Hide AQ Telemetry" : "Show AQ Telemetry";
optionsEnumArray[options++] = show_aq_telemetry;
optionsArray[options] = moduleConfig.telemetry.power_screen_enabled ? "Hide Power" : "Show Power";
optionsEnumArray[options++] = show_power;
@@ -2382,10 +2554,14 @@ void menuHandler::FrameToggles_menu()
screen->toggleFrameVisibility("show_favorites");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == show_telemetry) {
} else if (selected == show_env_telemetry) {
moduleConfig.telemetry.environment_screen_enabled = !moduleConfig.telemetry.environment_screen_enabled;
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == show_aq_telemetry) {
moduleConfig.telemetry.air_quality_screen_enabled = !moduleConfig.telemetry.air_quality_screen_enabled;
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == show_power) {
moduleConfig.telemetry.power_screen_enabled = !moduleConfig.telemetry.power_screen_enabled;
menuHandler::menuQueue = menuHandler::FrameToggles;
@@ -2442,6 +2618,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case radio_preset_picker:
RadioPresetPicker();
break;
case frequency_slot:
FrequencySlotPicker();
break;
case no_timeout_lora_picker:
LoraRegionPicker(0);
break;
@@ -2510,8 +2689,11 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case shutdown_menu:
shutdownMenu();
break;
case add_favorite:
addFavoriteMenu();
case NodePicker_menu:
NodePicker();
break;
case Manage_Node_menu:
ManageNodeMenu();
break;
case remove_favorite:
removeFavoriteMenu();

View File

@@ -13,6 +13,7 @@ class menuHandler
lora_picker,
device_role_picker,
radio_preset_picker,
frequency_slot,
no_timeout_lora_picker,
TZ_picker,
twelve_hour_picker,
@@ -33,7 +34,8 @@ class menuHandler
brightness_picker,
reboot_menu,
shutdown_menu,
add_favorite,
NodePicker_menu,
Manage_Node_menu,
remove_favorite,
test_menu,
number_test,
@@ -55,12 +57,14 @@ class menuHandler
DisplayUnits
};
static screenMenus menuQueue;
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
static void OnboardMessage();
static void LoraRegionPicker(uint32_t duration = 30000);
static void loraMenu();
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();
@@ -90,6 +94,8 @@ class menuHandler
static void BrightnessPickerMenu();
static void rebootMenu();
static void shutdownMenu();
static void NodePicker();
static void ManageNodeMenu();
static void addFavoriteMenu();
static void removeFavoriteMenu();
static void traceRouteMenu();
@@ -134,7 +140,7 @@ struct ScreenColor {
uint8_t b;
bool useVariant;
ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
explicit ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
: r(rIn), g(gIn), b(bIn), useVariant(variantIn)
{
}
@@ -149,6 +155,7 @@ using GPSToggleOption = MenuOption<meshtastic_Config_PositionConfig_GpsMode>;
using GPSFormatOption = MenuOption<meshtastic_DeviceUIConfig_GpsCoordinateFormat>;
using NodeNameOption = MenuOption<bool>;
using PositionMenuOption = MenuOption<int>;
using ManageNodeOption = MenuOption<int>;
using ClockFaceOption = MenuOption<bool>;
} // namespace graphics

View File

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

View File

@@ -176,6 +176,7 @@ int calculateMaxScroll(int totalEntries, int visibleRows)
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
{
x = (currentResolution == ScreenResolution::High) ? x - 2 : (currentResolution == ScreenResolution::Low) ? x - 1 : x;
for (int y = yStart; y <= yEnd; y += 2) {
display->setPixel(x, y);
}
@@ -205,9 +206,11 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
{
bool isLeftCol = (x < SCREEN_WIDTH / 2);
int nameMaxWidth = columnWidth - 25;
int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
const char *nodeName = getSafeNodeName(display, node, columnWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
char timeStr[10];
uint32_t seconds = sinceLastSeen(node);
@@ -234,6 +237,13 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
}
}
if (node->is_ignored || isMuted) {
if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else {
display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6);
}
}
int rightEdge = x + columnWidth - timeOffset;
if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time
@@ -253,6 +263,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
int barsXOffset = columnWidth - barsOffset;
const char *nodeName = getSafeNodeName(display, node, columnWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
@@ -265,6 +276,13 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
}
}
if (node->is_ignored || isMuted) {
if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else {
display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6);
}
}
// Draw signal strength bars
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
@@ -298,6 +316,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
const char *nodeName = getSafeNodeName(display, node, columnWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
char distStr[10] = "";
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
@@ -358,6 +377,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
}
}
if (node->is_ignored || isMuted) {
if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else {
display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6);
}
}
if (strlen(distStr) > 0) {
int offset = (currentResolution == ScreenResolution::High)
@@ -392,6 +418,7 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
const char *nodeName = getSafeNodeName(display, node, columnWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
@@ -403,6 +430,13 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
}
}
if (node->is_ignored || isMuted) {
if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else {
display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6);
}
}
}
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,

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
@@ -155,6 +155,18 @@ void InkHUD::LogoApplet::onShutdown()
// This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is complete
}
void InkHUD::LogoApplet::onApplyingChanges()
{
bringToForeground();
textLeft = "";
textRight = "";
textTitle = "Applying changes";
fontTitle = fontSmall;
inkhud->forceUpdate(Drivers::EInk::FAST, false);
}
void InkHUD::LogoApplet::onReboot()
{
bringToForeground();
@@ -164,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,11 +21,12 @@ 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;
void onReboot() override;
void onApplyingChanges();
protected:
int32_t runOnce() override;

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -32,9 +32,13 @@ 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);
protected:
Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton
@@ -50,20 +54,32 @@ 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
void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh
void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data
MenuPage startPageOverride = MenuPage::ROOT;
MenuPage currentPage = MenuPage::ROOT;
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<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
std::vector<std::string> nodeConfigLabels; // Persistent labels for Node Config pages
uint8_t selectedChannelIndex = 0; // Currently selected LoRa channel (Node Config → Radio → Channel)
bool channelPositionEnabled = false;
bool gpsEnabled = false;
// Recents menu checkbox state (derived from settings.recentlyActiveSeconds)
static constexpr uint8_t RECENTS_COUNT = 6;
bool recentsSelected[RECENTS_COUNT] = {};
// Data for selecting and sending canned messages via the menu
// Placed into a sub-class for organization only
@@ -94,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

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

View File

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

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

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

View File

@@ -23,6 +23,7 @@ class TipsApplet : public SystemApplet
enum class Tip {
WELCOME,
FINISH_SETUP,
PICK_REGION,
SAFE_SHUTDOWN,
CUSTOMIZATION,
BUTTONS,
@@ -32,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
@@ -276,6 +309,15 @@ int InkHUD::Events::beforeDeepSleep(void *unused)
return 0; // We agree: deep sleep now
}
// Display an intermediate screen while configuration changes are applied
void InkHUD::Events::applyingChanges()
{
// Bring the logo applet forward with a temporary message
for (SystemApplet *sa : inkhud->systemApplets) {
sa->onApplyingChanges();
}
}
// Callback for rebootObserver
// Same as shutdown, without drawing the logoApplet
// Makes sure we don't lose message history / InkHUD config

View File

@@ -29,12 +29,18 @@ class Events
void onButtonShort(); // User button: short press
void onButtonLong(); // User button: long press
void onExitShort(); // Exit button: short press
void onExitLong(); // Exit button: long press
void onNavUp(); // Navigate up
void onNavDown(); // Navigate down
void onNavLeft(); // Navigate left
void onNavRight(); // Navigate right
void applyingChanges();
void onExitShort(); // Exit button: short press
void onExitLong(); // Exit button: long press
void onNavUp(); // Navigate up
void onNavDown(); // Navigate down
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

View File

@@ -53,6 +53,13 @@ void InkHUD::InkHUD::addApplet(const char *name, Applet *a, bool defaultActive,
windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile);
}
void InkHUD::InkHUD::notifyApplyingChanges()
{
if (events) {
events->applyingChanges();
}
}
// Start InkHUD!
// Call this only after you have configured InkHUD
void InkHUD::InkHUD::begin()
@@ -168,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"
@@ -197,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()
@@ -245,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

@@ -47,6 +47,7 @@ class InkHUD
void setDriver(Drivers::EInk *driver);
void setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0);
void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, uint8_t onTile = -1);
void notifyApplyingChanges();
void begin();
@@ -62,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
@@ -70,17 +76,23 @@ class InkHUD
void prevApplet();
void openMenu();
void openAlignStick();
void openKeyboard();
void closeKeyboard();
void nextTile();
void prevTile();
void rotate();
void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default
void toggleBatteryIcon();
// Used by TipsApplet to force menu to start on Region selection
bool forceRegionMenu = false;
// Updating the display
// - 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,11 +22,14 @@ 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() {}
// Other system applets may take precedence over our own system applet though
// The order an applet is passed to WindowManager::addSystemApplet determines this hierarchy (added earlier = higher rank)
@@ -40,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;

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