Files
firmware/src/graphics/niche/InkHUD/WindowManager.cpp

1128 lines
41 KiB
C++
Raw Normal View History

2.6 changes (#5806) * 2.6 protos * [create-pull-request] automated change (#5789) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Hello world support for UDP broadcasts over the LAN on ESP32 (#5779) * UDP local area network meshing on ESP32 * Logs * Comment * Update UdpMulticastThread.h * Changes * Only use router->send * Make NodeDatabase (and file) independent of DeviceState (#5813) * Make NodeDatabase (and file) independent of DeviceState * 70 * Remove logging statement no longer needed * Explicitly set CAD symbols, improve slot time calculation and adjust CW size accordingly (#5772) * File system persistence fixes * [create-pull-request] automated change (#6000) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Update ref * Back to 80 * [create-pull-request] automated change (#6002) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * 2.6 <- Next hop router (#6005) * Initial version of NextHopRouter * Set original hop limit in header flags * Short-circuit to FloodingRouter for broadcasts * If packet traveled 1 hop, set `relay_node` as `next_hop` for the original transmitter * Set last byte to 0xFF if it ended at 0x00 As per an idea of @S5NC * Also update next-hop based on received DM for us * temp * Add 1 retransmission for intermediate hops when using NextHopRouter * Add next_hop and relayed_by in PacketHistory for setting next-hop and handle flooding fallback * Update protos, store multiple relayers * Remove next-hop update logic from NeighborInfoModule * Fix retransmissions * Improve ACKs for repeated packets and responses * Stop retransmission even if there's not relay node * Revert perhapsRebroadcast() * Remove relayer if we cancel a transmission * Better checking for fallback to flooding * Fix newlines in traceroute print logs * Stop retransmission for original packet * Use relayID * Also when want_ack is set, we should try to retransmit * Fix cppcheck error * Fix 'router' not in scope error * Fix another cppcheck error * Check for hop_limit and also update next hop when `hop_start == hop_limit` on ACK Also check for broadcast in `getNextHop()` * Formatting and correct NUM_RETRANSMISSIONS * Update protos * Start retransmissions in NextHopRouter if ReliableRouter didn't do it * Handle repeated/fallback to flooding packets properly First check if it's not still in the TxQueue * Guard against clients setting `next_hop`/`relay_node` * Don't cancel relay if we were the assigned next-hop * Replies (e.g. tapback emoji) are also a valid confirmation of receipt --------- Co-authored-by: GUVWAF <thijs@havinga.eu> Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> Co-authored-by: Tom Fifield <tom@tomfifield.net> Co-authored-by: GUVWAF <78759985+GUVWAF@users.noreply.github.com> * fix "native" compiler errors/warnings NodeDB.h * fancy T-Deck / SenseCAP Indicator / unPhone / PICOmputer-S3 TFT screen (#3259) * lib update: light theme * fix merge issue * lib update: home buttons + button try-fix * lib update: icon color fix * lib update: fix instability/crash on notification * update lib: timezone * timezone label * lib update: fix set owner * fix spiLock in RadioLibInterface * add picomputer tft build * picomputer build * fix compiler error std::find() * fix merge * lib update: theme runtime config * lib update: packet logger + T-Deck Plus * lib update: mesh detector * lib update: fix brightness & trackball crash * try-fix less paranoia * sensecap indicator updates * lib update: indicator fix * lib update: statistic & some fixes * lib-update: other T-Deck touch driver * use custom touch driver for Indicator * lower tft task prio * prepare LVGL ST7789 driver * lib update: try-fix audio * Drop received packets from self * Additional decoded packet ignores * Honor flip & color for Heltec T114 and T190 (#4786) * Honor TFT_MESH color if defined for Heltec T114 or T190 * Temporary: point lib_deps at fork of Heltec's ST7789 library For demo only, until ST7789 is merged * Update lib_deps; tidy preprocessor logic * Download debian files after firmware zip * set title for protobufs bump PR (#4792) * set title for version bump PR (#4791) * Enable Dependabot * chore: trunk fmt * fix dependabot syntax (#4795) * fix dependabot syntax * Update dependabot.yml * Update dependabot.yml * Bump peter-evans/create-pull-request from 6 to 7 in /.github/workflows (#4797) * Bump docker/build-push-action from 5 to 6 in /.github/workflows (#4800) * Actions: Semgrep Images have moved from returntocorp to semgrep (#4774) https://hub.docker.com/r/returntocorp/semgrep notes: "We've moved! Official Docker images for Semgrep now available at semgrep/semgrep." Patch updates our CI workflow for these images. Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Bump meshtestic from `31ee3d9` to `37245b3` (#4799) Bumps [meshtestic](https://github.com/meshtastic/meshTestic) from `31ee3d9` to `37245b3`. - [Commits](https://github.com/meshtastic/meshTestic/compare/31ee3d90c8bef61e835c3271be2c7cda8c4a5cc2...37245b3d612a9272f546bbb092837bafdad46bc2) --- updated-dependencies: - dependency-name: meshtestic dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [create-pull-request] automated change (#4789) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Bump pnpm/action-setup from 2 to 4 in /.github/workflows (#4798) Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 2 to 4. - [Release notes](https://github.com/pnpm/action-setup/releases) - [Commits](https://github.com/pnpm/action-setup/compare/v2...v4) --- updated-dependencies: - dependency-name: pnpm/action-setup dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Raspberry Pico2 - needs protos * Re-order doDeepSleep (#4802) Make sure PMU sleep takes place before I2C ends * [create-pull-request] automated change * heltec-wireless-bridge requires Proto PR first * feat: trigger class update when protobufs are changed * meshtastic/ is a test suite; protobufs/ contains protobufs; * Update platform-native to pick up portduino crash fix (#4807) * Hopefully extract and commit to meshtastic.github.io * CI fixes * [Board] DIY "t-energy-s3_e22" (#4782) * New variant "t-energy-s3_e22" - Lilygo T-Energy-S3 - NanoVHF "Mesh-v1.06-TTGO-T18" board - Ebyte E22 Series * add board_level = extra * Update variant.h --------- Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> Co-authored-by: Tom Fifield <tom@tomfifield.net> * Consolidate variant build steps (#4806) * poc: consolidate variant build steps * use build-variant action * only checkout once and clean up after run * Revert "Consolidate variant build steps (#4806)" (#4816) This reverts commit 9f8d86cb25febfb86c57f395549b7deb82458065. * Make Ublox code more readable (#4727) * Simplify Ublox code Ublox comes in a myriad of versions and settings. Presently our configuration code does a lot of branching based on versions being or not being present. This patch adds version detection earlier in the piece and branches on the set gnssModel instead to create separate setup methods for Ublox 6, Ublox 7/8/9, and Ublox10. Additionally, adds a macro to make the code much shorter and more readable. * Make trunk happy * Make trunk happy --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Consider the LoRa header when checking packet length * Minor fix (#4666) * Minor fixes It turns out setting a map value with the index notation causes an lookup that can be avoided with emplace. Apply this to one line in the StoreForward module. Fix also Cppcheck-determined highly minor performance increase by passing gpiochipname as a const reference :) The amount of cycles used on this laptop while learning about these callouts from cppcheck is unlikely to ever be more than the cycles saved by the fixes ;) * Update PortduinoGlue.cpp * Revert "Update classes on protobufs update" (#4824) * Revert "Update classes on protobufs update" * remove quotes to fix trunk. --------- Co-authored-by: Tom Fifield <tom@tomfifield.net> * Implement optional second I2C bus for NRF52840 Enabled at compile-time if WIRE_INFERFACES_COUNT defined as 2 * Add I2C bus to Heltec T114 header pins SDA: P0.13 SCL: P0.16 Uses bus 1, leaving bus 0 routed to the unpopulated footprint for the RTC (general future-proofing) * Tidier macros * Swap SDA and SCL SDA=P0.16, SCL=P0.13 * Refactor and consolidate time window logic (#4826) * Refactor and consolidate windowing logic * Trunk * Fixes * More * Fix braces and remove unused now variables. There was a brace in src/mesh/RadioLibInterface.cpp that was breaking compile on some architectures. Additionally, there were some brace errors in src/modules/Telemetry/AirQualityTelemetry.cpp src/modules/Telemetry/EnvironmentTelemetry.cpp src/mesh/wifi/WiFiAPClient.cpp Move throttle include in WifiAPClient.cpp to top. Add Default.h to sleep.cpp rest of files just remove unused now variables. * Remove a couple more meows --------- Co-authored-by: Tom Fifield <tom@tomfifield.net> * Rename message length headers and set payload max to 255 (#4827) * Rename message length headers and set payload max to 255 * Add MESHTASTIC_PKC_OVERHEAD * compare to MESHTASTIC_HEADER_LENGTH --------- Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> * Check for null before printing debug (#4835) * fix merge * try-fix crash * lib update: fix neighbors * fix GPIO0 mode after I2S audio * lib update: audio fix * lib update: fixes and improvements * extra * added ILI9342 (from master) * device-ui persistency * review update * fix request, add handled * fix merge issue * fix merge issue * remove newline * remove newlines from debug log * playing with locks; but needs more testing * diy mesh-tab initial files * board definition for mesh-tab (not yet used) * use DISPLAY_SET_RESOLUTION to avoid hw dependency in code * no telemetry for Indicator * 16MB partition for Indicator * 8MB partition for Indicator * stability: add SPI lock before saving via littleFS * dummy for config transfer (#5154) * update indicator (due to compile and linker errors) * remove faulty partition line * fix missing include * update indicator board * update mesh-tab ILI9143 TFT * fix naming * mesh-tab targets * try: disable duplicate locks * fix nodeDB erase loop when free mem returns invalid value (0, -1). * upgrade toolchain for nrf52 to gcc 9.3.1 * try-fix (workaround) T-Deck audio crash * update mesh-tab tft configs * set T-Deck audio to unused 48 (mem mclk) * swap mclk to gpio 21 * update meshtab voltage divider * update mesh-tab ini * Fixed the issue that indicator device uploads via rp2040 serial port in some cases. * Fixed the issue that the touch I2C address definition was not effective. * Fixed the issue that the wifi configuration saved to RAM did not take effect. * rotation fix; added ST7789 3.2" display * dreamcatcher: assign GPIO44 to audio mclk * mesh-tab touch updates * add mesh-tab powersave as default * fix DIO1 wakeup * mesh-tab: enable alert message menu * Streamline board definitions for first tech preview. (#5390) * Streamline board definitions for first tech preview. TBD: Indicator Support * add point-of-checkin * use board/unphone.json --------- Co-authored-by: mverch67 <manuel.verch@gmx.de> * fix native targets * add RadioLib debugging options for (T-Deck) * fix T-Deck build * fix native tft targets for rpi * remove wrong debug defines * t-deck-tft button is handled in device-ui * disable default lightsleep for indicator * Windows Support - Trunk and Platformio (#5397) * Add support for GPG * Add usb device support * Add trunk.io to devcontainer * Trunk things * trunk fmt * formatting * fix trivy/DS002, checkov/CKV_DOCKER_3 * hide docker extension popup * fix trivy/DS026, checkov/CKV_DOCKER_2 * fix radioLib warnings for T-Deck target * wake screen with button only * use custom touch driver * define wake button for unphone * use board definition for mesh-tab * mesh-tab rotation upside-down * update platform native * use MESH_TAB hardware model definition * radioLib update (fix crash/assert) * reference seeed indicator fix commit arduino-esp32 * Remove unneeded file change :) * disable serial module and tcp socket api for standalone devices (#5591) * disable serial module and tcp socket api for standalone devices * just disable webserver, leave wifi available * disable socket api * mesh-tab: lower I2C touch frequency * log error when packet queue is full * add more locking for shared SPI devices (#5595) * add more locking for shared SPI devices * call initSPI before the lock is used * remove old one * don't double lock * Add missing unlock * More missing unlocks * Add locks to SafeFile, remove from `readcb`, introduce some LockGuards * fix lock in setupSDCard() * pull radiolib trunk with SPI-CS fixes * change ContentHandler to Constructor type locks, where applicable --------- Co-authored-by: mverch67 <manuel.verch@gmx.de> Co-authored-by: GUVWAF <thijs@havinga.eu> Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com> * T-Deck: revert back to lovyanGFX touch driver * T-Deck: increase allocated PSRAM by 50% * mesh-tab: streamline target definitions * update RadioLib 7.1.2 * mesh-tab: fix touch rotation 4.0 inch display * Mesh-Tab platformio: 4.0inch: increase SPI frequency to max * mesh-tab: fix rotation for 3.5 IPS capacitive display * mesh-tab: fix rotation for 3.2 IPS capacitive display * restructure device-ui library into sub-directories * preparations for generic DisplayDriverFactory * T-Deck: increase LVGL memory size * update lib * trunk fmt --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: todd-herbert <herbert.todd@gmail.com> Co-authored-by: Jason Murray <15822260+scruplelesswizard@users.noreply.github.com> Co-authored-by: Jason Murray <jason@chaosaffe.io> Co-authored-by: Tom Fifield <tom@tomfifield.net> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Austin <vidplace7@gmail.com> Co-authored-by: virgil <virgil.wang.cj@gmail.com> Co-authored-by: Mark Trevor Birss <markbirss@gmail.com> Co-authored-by: Kalle Lilja <15094562+ThatKalle@users.noreply.github.com> Co-authored-by: GUVWAF <thijs@havinga.eu> * Version this * Update platformio.ini (#6006) * tested higher speed and it works * Un-extra * Add -tft environments to the ci matrix * Exclude unphone tft for now. Something is wonky * fixed Indicator touch issue (causing IO expander issues), added more RAM * update lib * fixed Indicator touch issue (causing IO expander issues), added more RAM (#6013) * increase T-Deck PSRAM to avoid too early out-of-memory when messages fill up the storage * update device-ui lib * Fix T-Deck SD card detection (#6023) * increase T-Deck PSRAM to avoid too early out-of-memory when messages fill up the storage * fix SDCard for T-Deck; allow SPI frequency config * meshtasticd: Add X11 480x480 preset (#6020) * Littlefs per device * 2.6 update * [create-pull-request] automated change (#6037) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * InkHUD UI for E-Ink (#6034) * Decouple ButtonThread from sleep.cpp Reorganize sleep observables. Don't call ButtonThread methods inside doLightSleep. Instead, handle in class with new lightsleep Observables. * InkHUD: initial commit (WIP) Publicly discloses the current work in progress. Not ready for use. * feat: battery icon * chore: implement meshtastic/firmware #5454 Clean up some inline functions * feat: menu & settings for "jump to applet" * Remove the beforeRender pattern It hugely complicates things. If we can achieve acceptable performance without it, so much the better. * Remove previous Map Applet Needs re-implementation to work without the beforeRender pattern * refactor: reimplement map applet Doesn't require own position Doesn't require the beforeRender pattern to precalculate; now all-at-once in render Lays groundwork for fixed-size map with custom background image * feat: autoshow Allow user to select which applets (if any) should be automatically brought to foreground when they have new data to display * refactor: tidy-up applet constructors misc. jobs including: - consistent naming - move initializer-list-only constructors to header - give derived applets unique identifiers for MeshModule and OSThread logging * hotfix: autoshow always uses FAST update In future, it *will* often use FAST, but this will be controlled by a WindowManager component which has not yet been written. Hotfixed, in case anybody is attempting to use this development version on their deployed devices. * refactor: bringToForeground no longer requests FAST update In situations where an applet has moved to foreground because of user input, requestUpdate can be manually called, to upgrade to FAST refresh. More permanent solution for #23e1dfc * refactor: extract string storage from ThreadedMessageApplet Separates the code responsible for storing the limited message history, which was previously part of the ThreadedMessageApplet. We're now also using this code to store the "most recent message". Previously, this was stored in the `InkHUD::settings` struct, which was much less space-efficient. We're also now storing the latest DM, laying the foundation for an applet to display only DMs, which will complement the threaded message applet. * fix: text wrapping Attempts to fix a disparity between `Applet::printWrapped` and `Applet::getWrappedTextHeight`, which would occasionally cause a ThreadedMessageApplet message to render "too short", overlapping other text. * fix: purge old constructor This one slipped through the last commit.. * feat: DM Applet Useful in combination with the ThreadedMessageApplets, which don't show DMs * fix: applets shouldn't handle events while deactivated Only one or two applets were actually doing this, but I'm making a habit of having all applets return early from their event handling methods (as good practice), even if those methods are disabled elsewhere (e.g. not observing observable, return false from wantPacket) * refactor: allow requesting update without requesting autoshow Some applets may want to redraw, if they are displayed, but not feel the information is worth being brought to foreground for. Example: ActiveNodesApplet, when purging old nodes from list. * feat: custom "Recently Active" duration Allows users to tailor how long nodes will appear in the "Recents" applets, to suit the activity level of their mesh. * refactor: rename some applets * fix: autoshow * fix: getWrappedTextHeight Remove the "simulate" option from printWrapped; too hard to keep inline with genuine printing (because of AdafruitGFX Fonts' xAdvance, mabye?). Instead of simulating, we printWrapped as normal, and discard pixel output by setting crop. Both methods are similarly inefficient, apparently. * fix: text wrapping in ThreadedMessageApplet Wrong arguments were passed to Applet::printWrapped * feat: notifications for text messages Only shown if current applet does not already display the same info. Autoshow takes priority over notifications, if both would be used to display the same info. * feat: optimize FAST vs FULL updates New UpdateMediator class counts the number of each update type, and suggets which one to use, if the code doesn't already have an explicit prefence. Also performs "maintenance refreshes" unprovoked if display is not given an opportunity to before a FULL refresh through organic use. * chore: update todo list * fix: rare lock-up of buttons * refactor: backlight Replaces the initial proof-of-concept frontlight code for T-Echo Presses less than 5 seconds momentarily illuminate the display Presses longer than 5 seconds latch the light, requiring another tap to disable If user has previously removed the T-Echo's capacitive touch button (some DIY projects), the light is controlled by the on-screen menu. This fallback is used by all T-Echo devices, until a press of the capacitive touch button is detected. * feat: change tile with aux button Applied to VM-E290. Working as is, but a refactor of WindowManager::render is expected shortly, which will also tidy code from this push. * fix: specify out-of-the-box tile assignments Prevents placeholder applet showing on initial boot, for devices which use a mult-tile layout by default (VM-E290) * fix: verify settings version when loading * fix: wrong settings version * refactor: remove unimplemented argument from requestUpdate Specified whether or not to update "async", however the implementation was slightly broken, Applet::requestUpdate is only handled next time WindowManager::runOnce is called. This didn't allow code to actually await an update, which was misleading. * refactor: renaming Applet::render becomes Applet::onRender. Tile::displayedApplet becomes Tile::assignedApplet. New onRender method name allows us to move some of the pre and post render code from WindowManager into new Applet::render method, which will call onRender for us. * refactor: rendering Bit of a tidy-up. No intended change in behavior. * fix: optimize refresh times Shorter wait between retrying update if display was previously busy. Set anticipated update durations closer to observed values. No signifacant performance increase, but does decrease the amount of polling required. * feat: blocking update for E-Ink Option to wait for display update to complete before proceeding. Important when shutting down the device. * refactor: allow system applets to lock rendering Temporarily prevents other applets from rendering. * feat: boot and shutdown screens * feat: BluetoothStatus Adds a meshtastic::Status object which exposes the state of the Bluetooth connection. Intends to allow decoupling of UI code. * feat: Bluetooth pairing screen * fix: InkHUD defaults not honored * fix: random Bluetooth pin for NicheGraphics UIs * chore: button interrupts tested * fix: emoji reactions show as blank messages * fix: autoshow and notification triggered by outgoing message * feat: save InkHUD data before reboot Implemented with a new Observable. Previously, config and a few recent messages were saved on shutdown. These were lost if the device rebooted, for example when firmware settings were changed by a client. Now, the InkHUD config and recent messages saved on reboot, the same as during an intentional shutdown. * feat: imperial distances Controlled by the config.display.units setting * fix: hide features which are not yet implemented * refactor: faster rendering Previously, only tiles which requested update were re-rendered. Affected tiles had their region blanked before render, pixel by pixel. Benchmarking revealed that it is significantly faster to memset the framebuffer and redraw all tiles. * refactor: tile ownership Tiles and Applets now maintain a reciprocal link, which is enforced by asserts. Less confusing than the old situation, where an applet and a tile may disagree on their relationship. Empty tiles are now identified by a nullptr *Applet, instead of by having the placeholderApplet assigned. * fix: notifications and battery when menu open Do render notifications in front of menu; don't render battery icon in front of menu. * fix: simpler defaults Don't expose new users to multiplexed applets straight away: make them enable the feature for themselves. * fix: Inputs::TwoButton interrupts, when only one button in use * fix: ensure display update is complete when ESP32 enters light sleep Many panels power down automatically, but some require active intervention from us. If light sleep (ESP32) occurs during a display update, these panels could potentially remain powered on, applying voltage the pixels for an extended period of time, and potentially damaging the display. * fix: honor per-variant user tile limit Set as the default value for InkHUD::settings.userTiles.maxCount in nicheGraphics.h * feat: initial InkHUD support for Wireless Paper v1.1 and VM-E213 * refactor: Heard and Recents Applets Tidier code, significant speed boost. Possibly no noticable change in responsiveness, but rendering now spends much less time blocking execution, which is important for correction functioning of the other firmware components. * refactor: use a common pio base config Easier to make any future PlatformIO config changes * feat: tips Show information that we think the user might find helpful. Some info shown first boot only. Other info shown when / if relevant. * fix: text wrapping for '\n' Previously, the newline was honored, but the adojining word was not printed. * Decouple ButtonThread from sleep.cpp Reorganize sleep observables. Don't call ButtonThread methods inside doLightSleep. Instead, handle in class with new lightsleep Observables. * feat: BluetoothStatus Adds a meshtastic::Status object which exposes the state of the Bluetooth connection. Intends to allow decoupling of UI code. * feat: observable for reboot * refactor: Heltec VM-E290 installDefaultConfig * fix: random Bluetooth pin for NicheGraphics UIs * update device-ui: fix touch/crash issue while light sleep * Collect inkhud * fix: InkHUD shouldn't nag about timezone (#6040) * Guard eink drivers w/ MESHTASTIC_INCLUDE_NICHE_GRAPHICS * Case sensitive perhaps? * More case-sensitivity instances * Moar * RTC * Yet another case issue! * Sigh... * MUI: BT programming mode (#6046) * allow BT connection with disabled MUI * Update device-ui --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * MUI: fix nag timeout, disable BT programming mode for native (#6052) * allow BT connection with disabled MUI * Update device-ui * MUI: fix nag timeout default and remove programming mode for native --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * remove debuglog leftover * Wireless Paper: remove stray board_level = extra (#6060) Makes sure the InkHUD version gets build into the release zip * Fixed persistence stragglers from NodeDB / Device State divorce (#6059) * Increase `MAX_THREADS` for InkHUD variants with WiFi (#6064) * Licensed usage compliance (#6047) * Prevent psk and legacy admin channel on licensed mode * Move it * Consolidate warning strings * More holes * Device UI submodule bump * Prevent licensed users from rebroadcasting unlicensed traffic (#6068) * Prevent licensed users from rebroadcasting unlicensed traffic * Added method and enum to make user license status more clear * MUI: move UI initialization out of main.cpp and adding lightsleep observer + mutex (#6078) * added device-ui to lightSleep observers for handling graceful sleep; refactoring main.cpp * bump lib version * Update device-ui * unPhone TFT: include into build, enable SD card, increase PSRAM (#6082) * unPhone-tft: include into build, enable SD card, increase assigned PSRAM * lib update * Backup / migrate pub private keys when upgrading to new files in 2.6 (#6096) * Save a backup of pub/private keys before factory reset * Fix licensed mode warning * Unlock spi on else file doesn't exist * Update device-ui * Update protos and device-ui * [create-pull-request] automated change (#6129) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Proto * [create-pull-request] automated change (#6131) * Proto update for backup * [create-pull-request] automated change (#6133) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Update protobufs * Space * [create-pull-request] automated change (#6144) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Protos * [create-pull-request] automated change (#6152) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Updeet * device-ui lib update * fix channel OK button * device-lib update: fix settings panel -> no scrolling * device-ui lib: last minute update * defined(SENSECAP_INDICATOR) * MUI hot-fix pub/priv keys * MUI hot-fix username dialog * MUI: BT programming mode button * Update protobufs --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: GUVWAF <78759985+GUVWAF@users.noreply.github.com> Co-authored-by: GUVWAF <thijs@havinga.eu> Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> Co-authored-by: Tom Fifield <tom@tomfifield.net> Co-authored-by: mverch67 <manuel.verch@gmx.de> Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com> Co-authored-by: todd-herbert <herbert.todd@gmail.com> Co-authored-by: Jason Murray <15822260+scruplelesswizard@users.noreply.github.com> Co-authored-by: Jason Murray <jason@chaosaffe.io> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Austin <vidplace7@gmail.com> Co-authored-by: virgil <virgil.wang.cj@gmail.com> Co-authored-by: Mark Trevor Birss <markbirss@gmail.com> Co-authored-by: Kalle Lilja <15094562+ThatKalle@users.noreply.github.com> Co-authored-by: rcarteraz <robert.l.carter2@gmail.com>
2025-03-01 06:18:33 -06:00
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./WindowManager.h"
#include "RTC.h"
#include "mesh/NodeDB.h"
// System applets
// Must be defined in .cpp to prevent a circular dependency with Applet base class
#include "./Applets/System/BatteryIcon/BatteryIconApplet.h"
#include "./Applets/System/Logo/LogoApplet.h"
#include "./Applets/System/Menu/MenuApplet.h"
#include "./Applets/System/Notification/NotificationApplet.h"
#include "./Applets/System/Pairing/PairingApplet.h"
#include "./Applets/System/Placeholder/PlaceholderApplet.h"
#include "./Applets/System/Tips/TipsApplet.h"
using namespace NicheGraphics;
InkHUD::WindowManager::WindowManager() : concurrency::OSThread("InkHUD WM")
{
// Nothing for the timer to do just yet
OSThread::disable();
}
// Get or create the WindowManager singleton
InkHUD::WindowManager *InkHUD::WindowManager::getInstance()
{
// Create the singleton instance of our class, if not yet done
static InkHUD::WindowManager *instance = new InkHUD::WindowManager();
return instance;
}
// Connect the driver, which is created independently is setupNicheGraphics()
void InkHUD::WindowManager::setDriver(Drivers::EInk *driver)
{
// Make sure not already set
if (this->driver) {
LOG_ERROR("Driver already set");
delay(2000); // Wait for native serial..
assert(false);
}
// Store the driver which was created in setupNicheGraphics()
this->driver = driver;
// Determine the dimensions of the image buffer, in bytes.
// Along rows, pixels are stored 8 per byte.
// Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these.
imageBufferWidth = ((driver->width - 1) / 8) + 1;
imageBufferHeight = driver->height;
// Allocate the image buffer
imageBuffer = new uint8_t[imageBufferWidth * imageBufferHeight];
}
// Sets the ideal ratio of FAST updates to FULL updates
// We want as many FAST updates as possible, without causing gradual degradation of the display
// If explicitly requested, number of FAST updates may exceed fastPerFull value.
// In this case, the stressMultiplier is applied, causing the "FULL update debt" to increase by more than normal
// The stressMultplier helps the display recover from particularly taxing periods of use
// (Default arguments of 5,2 are very conservative values)
void InkHUD::WindowManager::setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0)
{
mediator.fastPerFull = fastPerFull;
mediator.stressMultiplier = stressMultiplier;
}
// Register a user applet with the WindowManager
// This is called in setupNicheGraphics()
// This should be the only time that specific user applets are mentioned in the code
// If a user applet is not added with this method, its code should not be built
void InkHUD::WindowManager::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile)
{
userApplets.push_back(a);
// If requested, mark in settings that this applet should be active by default
// This means that it will be available for the user to cycle to with short-press of the button
// This is the default state only: user can activate or deactive applets through the menu.
// User's choice of active applets is stored in settings, and will be honored instead of these defaults, if present
if (defaultActive)
settings.userApplets.active[userApplets.size() - 1] = true;
// If requested, mark in settings that this applet should "autoshow" by default
// This means that the applet will be automatically brought to foreground when it has new data to show
// This is the default state only: user can select which applets have this behavior through the menu
// User's selection is stored in settings, and will be honored instead of these defaults, if present
if (defaultAutoshow)
settings.userApplets.autoshow[userApplets.size() - 1] = true;
// If specified, mark this as the default applet for a given tile index
// Used only to avoid placeholder applet "out of the box", when default settings have more than one tile
if (onTile != (uint8_t)-1)
settings.userTiles.displayedUserApplet[onTile] = userApplets.size() - 1;
// The label that will be show in the applet selection menu, on the device
a->name = name;
}
// Perform initial setup, and begin responding to incoming events
// First task once init is to show the boot screen
void InkHUD::WindowManager::begin()
{
// Make sure we have set a driver
if (!this->driver) {
LOG_ERROR("Driver not set");
delay(2000); // Wait for native serial..
assert(false);
}
loadDataFromFlash();
createSystemApplets();
createSystemTiles();
placeSystemTiles();
assignSystemAppletsToTiles();
createUserApplets();
createUserTiles();
placeUserTiles();
assignUserAppletsToTiles();
refocusTile();
logoApplet->showBootScreen();
forceUpdate(Drivers::EInk::FULL, false); // Update now, and wait here until complete
deepSleepObserver.observe(&notifyDeepSleep);
rebootObserver.observe(&notifyReboot);
textMessageObserver.observe(textMessageModule);
#ifdef ARCH_ESP32
lightSleepObserver.observe(&notifyLightSleep);
#endif
}
// Set-up special "system applets"
// These handle things like bootscreen, pop-up notifications etc
// They are processed separately from the user applets, because they might need to do "weird things"
// They also won't be activated or deactivated
void InkHUD::WindowManager::createSystemApplets()
{
logoApplet = new LogoApplet;
pairingApplet = new PairingApplet;
tipsApplet = new TipsApplet;
notificationApplet = new NotificationApplet;
batteryIconApplet = new BatteryIconApplet;
menuApplet = new MenuApplet;
placeholderApplet = new PlaceholderApplet;
// System applets are always active
logoApplet->activate();
pairingApplet->activate();
tipsApplet->activate();
notificationApplet->activate();
batteryIconApplet->activate();
menuApplet->activate();
placeholderApplet->activate();
// Add to the systemApplets vector
// Although system applets often need special handling, sometimes we can process them en-masse with this vector
// e.g. rendering, raising events
// Order of these entries determines Z-Index when rendering
systemApplets.push_back(logoApplet);
systemApplets.push_back(pairingApplet);
systemApplets.push_back(tipsApplet);
systemApplets.push_back(batteryIconApplet);
systemApplets.push_back(menuApplet);
systemApplets.push_back(notificationApplet);
// Note: placeholder applet is technically a system applet, but it renders in WindowManager::renderPlaceholders
}
void InkHUD::WindowManager::createSystemTiles()
{
fullscreenTile = new Tile;
notificationTile = new Tile;
batteryIconTile = new Tile;
}
void InkHUD::WindowManager::placeSystemTiles()
{
fullscreenTile->placeSystemTile(0, 0, getWidth(), getHeight());
notificationTile->placeSystemTile(0, 0, getWidth(), 20); // Testing only: constant value
// Todo: appropriate sizing for the battery icon
const uint16_t batteryIconHeight = Applet::getHeaderHeight() - (2 * 2);
uint16_t batteryIconWidth = batteryIconHeight * 1.8;
batteryIconTile->placeSystemTile(getWidth() - batteryIconWidth, 2, batteryIconWidth, batteryIconHeight);
}
// Assign a system applet to the fullscreen tile
// Rendering of user tiles is suspended when the fullscreen tile is occupied
void InkHUD::WindowManager::claimFullscreen(InkHUD::Applet *a)
{
// Make sure that only system applets use the fullscreen tile
bool isSystemApplet = false;
for (Applet *sa : systemApplets) {
if (sa == a) {
isSystemApplet = true;
break;
}
}
assert(isSystemApplet);
fullscreenTile->assignApplet(a);
}
// Clear the fullscreen tile, unlinking whichever system applet is assigned
// This allows the normal rendering of user tiles to resume
void InkHUD::WindowManager::releaseFullscreen()
{
// Make sure the applet is ready to release the tile
assert(!fullscreenTile->getAssignedApplet()->isForeground());
// Break the link between the applet and the fullscreen tile
fullscreenTile->assignApplet(nullptr);
}
// Some system applets can be assigned to a tile at boot
// These are applets which do have their own tile, and whose assignment never changes
// Applets which:
// - share the fullscreen tile (e.g. logoApplet, pairingApplet),
// - render on user tiles (e.g. menuApplet, placeholderApplet),
// are assigned to the tile only when needed
void InkHUD::WindowManager::assignSystemAppletsToTiles()
{
notificationTile->assignApplet(notificationApplet);
batteryIconTile->assignApplet(batteryIconApplet);
}
// Activate or deactivate user applets, to match settings
// Called at boot, or after run-time config changes via menu
// Note: this method does not instantiate the applets;
// this is done in setupNicheGraphics, with WindowManager::addApplet
void InkHUD::WindowManager::createUserApplets()
{
// Deactivate and remove any no-longer-needed applets
for (uint8_t i = 0; i < userApplets.size(); i++) {
Applet *a = userApplets.at(i);
// If the applet is active, but settings say it shouldn't be:
// - run applet's custom deactivation code
// - mark applet as inactive (internally)
if (a->isActive() && !settings.userApplets.active[i])
a->deactivate();
}
// Activate and add any new applets
for (uint8_t i = 0; i < userApplets.size() && i < MAX_USERAPPLETS_GLOBAL; i++) {
// If not activated, but it now should be:
// - run applet's custom activation code
// - mark applet as active (internally)
if (!userApplets.at(i)->isActive() && settings.userApplets.active[i])
userApplets.at(i)->activate();
}
}
void InkHUD::WindowManager::createUserTiles()
{
// Delete any tiles which currently exist
for (Tile *t : userTiles)
delete t;
userTiles.clear();
// Create new tiles
for (uint8_t i = 0; i < settings.userTiles.count; i++) {
Tile *t = new Tile;
userTiles.push_back(t);
}
}
void InkHUD::WindowManager::placeUserTiles()
{
// Calculate the display region occupied by each tile
// This determines how pixels are translated from applet-space to windowmanager-space
for (uint8_t i = 0; i < userTiles.size(); i++)
userTiles.at(i)->placeUserTile(settings.userTiles.count, i);
}
void InkHUD::WindowManager::assignUserAppletsToTiles()
{
// Set "assignedApplet" property
// Which applet should be initially shown on a tile?
// This is preserved between reboots, but the value needs validating at startup
for (uint8_t i = 0; i < userTiles.size(); i++) {
Tile *t = userTiles.at(i);
// Check whether tile can display the previously shown applet again
uint8_t oldIndex = settings.userTiles.displayedUserApplet[i]; // Previous index in WindowManager::userApplets
bool canRestore = true;
if (oldIndex > userApplets.size() - 1) // Check if old index is now out of bounds
canRestore = false;
else if (!settings.userApplets.active[oldIndex]) // Check that old applet is still activated
canRestore = false;
else { // Check that the old applet isn't now shown already on a different tile
for (uint8_t i2 = 0; i2 < i; i2++) {
if (settings.userTiles.displayedUserApplet[i2] == oldIndex) {
canRestore = false;
break;
}
}
}
// Restore previously shown applet if possible,
// otherwise assign nullptr, which will render specially using placeholderApplet
if (canRestore) {
Applet *a = userApplets.at(oldIndex);
t->assignApplet(a);
a->bringToForeground();
} else {
t->assignApplet(nullptr);
settings.userTiles.displayedUserApplet[i] = -1; // Update settings: current tile has no valid applet
}
}
}
void InkHUD::WindowManager::refocusTile()
{
// Validate "focused tile" setting
// - info: focused tile responds to button presses: applet cycling, menu, etc
// - if number of tiles changed, might now be out of index
if (settings.userTiles.focused >= userTiles.size())
settings.userTiles.focused = 0;
// Give "focused tile" a valid applet
// - scan for another valid applet, which we can addSubstitution
// - reason: nextApplet() won't cycle if no applet is assigned
Tile *focusedTile = userTiles.at(settings.userTiles.focused);
if (!focusedTile->getAssignedApplet()) {
// Search for available applets
for (uint8_t i = 0; i < userApplets.size(); i++) {
Applet *a = userApplets.at(i);
if (a->isActive() && !a->isForeground()) {
// Found a suitable applet
// Assign it to the focused tile
focusedTile->assignApplet(a);
a->bringToForeground();
settings.userTiles.displayedUserApplet[settings.userTiles.focused] = i; // Record change: persist after reboot
break;
}
}
}
}
// Callback for deepSleepObserver
// Returns 0 to signal that we agree to sleep now
int InkHUD::WindowManager::beforeDeepSleep(void *unused)
{
// Notify all applets that we're shutting down
for (Applet *ua : userApplets) {
ua->onDeactivate();
ua->onShutdown();
}
for (Applet *sa : userApplets) {
// Note: no onDeactivate. System applets are always active.
sa->onShutdown();
}
// User has successfull executed a safe shutdown
// We don't need to nag at boot anymore
settings.tips.safeShutdownSeen = true;
saveDataToFlash();
// Display the shutdown screen, and wait here until the update is complete
logoApplet->showShutdownScreen();
forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
return 0; // We agree: deep sleep now
}
// Callback for rebootObserver
// Same as shutdown, without drawing the logoApplet
// Makes sure we don't lose message history / InkHUD config
int InkHUD::WindowManager::beforeReboot(void *unused)
{
// Notify all applets that we're "shutting down"
// They don't need to know that it's really a reboot
for (Applet *a : userApplets) {
a->onDeactivate();
a->onShutdown();
}
for (Applet *sa : userApplets) {
// Note: no onDeactivate. System applets are always active.
sa->onShutdown();
}
saveDataToFlash();
return 0; // No special status to report. Ignored anyway by this Observable
}
#ifdef ARCH_ESP32
// Callback for lightSleepObserver
// Make sure the display is not partway through an update when we begin light sleep
// This is because some displays require active input from us to terminate the update process, and protect the panel hardware
int InkHUD::WindowManager::beforeLightSleep(void *unused)
{
if (driver->busy()) {
LOG_INFO("Waiting for display");
driver->await(); // Wait here for update to complete
}
return 0; // No special status to report. Ignored anyway by this Observable
}
#endif
// Callback when a new text message is received
// Caches the most recently received message, for use by applets
// Rx does not trigger a save to flash, however the data *will* be saved alongside other during shutdown, etc.
// Note: this is different from devicestate.rx_text_message, which may contain an *outgoing* message
int InkHUD::WindowManager::onReceiveTextMessage(const meshtastic_MeshPacket *packet)
{
// Short circuit: don't store outgoing messages
if (getFrom(packet) == nodeDB->getNodeNum())
return 0;
// Short circuit: don't store "emoji reactions"
// Possibly some implemetation of this in future?
if (packet->decoded.emoji)
return 0;
// Determine whether the message is broadcast or a DM
// Store this info to prevent confusion after a reboot
// Avoids need to compare timestamps, because of situation where "future" messages block newly received, if time not set
latestMessage.wasBroadcast = isBroadcast(packet->to);
// Pick the appropriate variable to store the message in
MessageStore::Message *storedMessage = latestMessage.wasBroadcast ? &latestMessage.broadcast : &latestMessage.dm;
// Store nodenum of the sender
// Applets can use this to fetch user data from nodedb, if they want
storedMessage->sender = packet->from;
// Store the time (epoch seconds) when message received
storedMessage->timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
// Store the channel
// - (potentially) used to determine whether notification shows
// - (potentially) used to determine which applet to focus
storedMessage->channelIndex = packet->channel;
// Store the text
// Need to specify manually how many bytes, because source not null-terminated
storedMessage->text =
std::string(&packet->decoded.payload.bytes[0], &packet->decoded.payload.bytes[packet->decoded.payload.size]);
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
}
// Triggered by an input source when a short-press fires
// The input source is a separate component; not part of InkHUD
// It is connected in setupNicheGraphics()
void InkHUD::WindowManager::handleButtonShort()
{
// If notification is open: close it
if (notificationApplet->isForeground()) {
notificationApplet->dismiss();
forceUpdate(EInk::UpdateTypes::FULL); // Redraw everything, to clear the notification
}
// If window manager is locked: lock owner handles button
else if (lockOwner)
lockOwner->onButtonShortPress();
// Normally: next applet
else
nextApplet();
}
// Triggered by an input source when a long-press fires
// The input source is a separate component; not part of InkHUD
// It is connected in setupNicheGraphics()
// Note: input source should raise this while button still held
void InkHUD::WindowManager::handleButtonLong()
{
if (lockOwner)
lockOwner->onButtonLongPress();
else
menuApplet->show(userTiles.at(settings.userTiles.focused));
}
// 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()
{
Tile *t = userTiles.at(settings.userTiles.focused);
// Abort if zero applets available
// nullptr means WindowManager::refocusTile determined that there were no available applets
if (!t->getAssignedApplet())
return;
// Find the index of the applet currently shown on the tile
uint8_t appletIndex = -1;
for (uint8_t i = 0; i < userApplets.size(); i++) {
if (userApplets.at(i) == t->getAssignedApplet()) {
appletIndex = i;
break;
}
}
// Confirm that we did find the applet
assert(appletIndex != (uint8_t)-1);
// Iterate forward through the WindowManager::applets, looking for the next valid applet
Applet *nextValidApplet = nullptr;
// for (uint8_t i = (appletIndex + 1) % applets.size(); i != appletIndex; i = (i + 1) % applets.size()) {
for (uint8_t i = 1; i < userApplets.size(); i++) {
uint8_t newAppletIndex = (appletIndex + i) % userApplets.size();
Applet *a = userApplets.at(newAppletIndex);
// Looking for an applet which is active (enabled by user), but currently in background
if (a->isActive() && !a->isForeground()) {
nextValidApplet = a;
settings.userTiles.displayedUserApplet[settings.userTiles.focused] =
newAppletIndex; // Remember this setting between boots!
break;
}
}
// Confirm that we found another applet
if (!nextValidApplet)
return;
// Hide old applet, show new applet
t->getAssignedApplet()->sendToBackground();
t->assignApplet(nextValidApplet);
nextValidApplet->bringToForeground();
forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST
}
// Focus on a different tile
// The "focused tile" is the one which cycles applets on user button press,
// and the one where the menu will be displayed
// Note: this method is only used by an aux button
// The menuApplet manually performs a subset of these actions, to avoid disturbing the stale image on adjacent tiles
void InkHUD::WindowManager::nextTile()
{
// Close the menu applet if open
// We done *really* want to do this, but it simplifies handling *a lot*
if (menuApplet->isForeground())
menuApplet->sendToBackground();
// Seems like some system applet other than menu is open. Pairing? Booting?
if (!canRequestUpdate())
return;
// Swap to next tile
settings.userTiles.focused = (settings.userTiles.focused + 1) % settings.userTiles.count;
// Make sure that we don't get stuck on the placeholder tile
// changeLayout reassigns applets to tiles
changeLayout();
// Ask the tile to draw an indicator showing which tile is now focused
// Requests a render
userTiles.at(settings.userTiles.focused)->requestHighlight();
}
// Perform necessary reconfiguration when user changes number of tiles (or rotation) at run-time
// Call after changing settings.tiles.count
void InkHUD::WindowManager::changeLayout()
{
// Recreate tiles
// - correct number created, from settings.userTiles.count
// - set dimension and position of tiles, according to layout
createUserTiles();
placeUserTiles();
placeSystemTiles();
// Handle fewer tiles
// - background any applets which have lost their tile
findOrphanApplets();
// Handle more tiles
// - create extra applets
// - assign them to the new extra tiles
createUserApplets();
assignUserAppletsToTiles();
// Focus a valid tile
// - info: focused tile is the one which cycles applets when user button pressed
// - may now be out of bounds if tile count has decreased
refocusTile();
// Restore menu
// - its tile was just destroyed and recreated (createUserTiles)
// - its assignment was cleared (assignUserAppletsToTiles)
if (menuApplet->isForeground()) {
Tile *ft = userTiles.at(settings.userTiles.focused);
menuApplet->show(ft);
}
// Force-render
// - redraw all applets
forceUpdate(EInk::UpdateTypes::FAST);
}
// Perform necessary reconfiguration when user activates or deactivates applets at run-time
// Call after changing settings.userApplets.active
void InkHUD::WindowManager::changeActivatedApplets()
{
assert(menuApplet->isForeground());
// Activate or deactivate applets
// - to match value of settings.userApplets.active
createUserApplets();
// Assign the placeholder applet
// - if applet was foreground on a tile when deactivated, swap it with a placeholder
// - placeholder applet may be assigned to multiple tiles, if needed
assignUserAppletsToTiles();
// Ensure focused tile has a valid applet
// - if focused tile's old applet was deactivated, give it a real applet, instead of placeholder
// - reason: nextApplet() won't cycle applets if placeholder is shown
refocusTile();
// Restore menu
// - its assignment was cleared (assignUserAppletsToTiles)
if (menuApplet->isForeground()) {
Tile *ft = userTiles.at(settings.userTiles.focused);
menuApplet->show(ft);
}
// Force-render
// - redraw all applets
forceUpdate(EInk::UpdateTypes::FAST);
}
// Change whether the battery icon is displayed (top left corner)
// Don't toggle the OptionalFeatures value before calling this, our method handles it internally
void InkHUD::WindowManager::toggleBatteryIcon()
{
assert(batteryIconApplet->isActive());
settings.optionalFeatures.batteryIcon = !settings.optionalFeatures.batteryIcon; // Preserve the change between boots
// Show or hide the applet
if (settings.optionalFeatures.batteryIcon)
batteryIconApplet->bringToForeground();
else
batteryIconApplet->sendToBackground();
// Force-render
// - redraw all applets
forceUpdate(EInk::UpdateTypes::FAST);
}
// Allow applets to suppress notifications
// Applets will be asked whether they approve, before a notification is shown via the NotificationApplet
// An applet might want to suppress a notification if the applet itself already displays this info
// Example: AllMessageApplet should not approve notifications for messages, if it is in foreground
bool InkHUD::WindowManager::approveNotification(InkHUD::Notification &n)
{
// Ask all currently displayed applets
for (Tile *ut : userTiles) {
Applet *ua = ut->getAssignedApplet();
if (ua && !ua->approveNotification(n))
return false;
}
// Nobody objected
return true;
}
// 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::WindowManager::requestUpdate()
{
requestingUpdate = true;
// We will run the thread as soon as we loop(),
// after all Applets have had a chance to observe whatever event set this off
OSThread::setIntervalFromNow(0);
OSThread::enabled = true;
runASAP = true;
}
// requestUpdate will not actually update if no requests were made by applets which are actually visible
// This can occur, because applets requestUpdate even from the background,
// in case the user's autoshow settings permit them to be moved to foreground.
// Sometimes, however, we will want to trigger a display update manually, in the absense of any sort of applet event
// Display health, for example.
// In these situations, we use forceUpdate
void InkHUD::WindowManager::forceUpdate(EInk::UpdateTypes type, bool async)
{
requestingUpdate = true;
forcingUpdate = true;
forcedUpdateType = type;
// Normally, we need to start the timer, in case the display is busy and we briefly defer the update
if (async) {
// We will run the thread as soon as we loop(),
// after all Applets have had a chance to observe whatever event set this off
OSThread::setIntervalFromNow(0);
OSThread::enabled = true;
runASAP = true;
}
// If the update is *not* asynchronous, we begin the render process directly here
// so that it can block code flow while running
else
render(false);
}
// Receives rendered image data from an Applet, via a tile
// When applets render, they output pixel data relative to their own left / top edges
// They pass this pixel data to tile, which offsets the pixels, making them relative to the display left / top edges
// That data is then passed to this method, which applies any rotation, then places the pixels into the image buffer
// That image buffer is the fully-formatted data handed off to the driver
void InkHUD::WindowManager::handleTilePixel(int16_t x, int16_t y, Color c)
{
rotatePixelCoords(&x, &y);
setBufferPixel(x, y, c);
}
// Width of the display, relative to rotation
uint16_t InkHUD::WindowManager::getWidth()
{
if (settings.rotation % 2)
return driver->height;
else
return driver->width;
}
// Height of the display, relative to rotation
uint16_t InkHUD::WindowManager::getHeight()
{
if (settings.rotation % 2)
return driver->width;
else
return driver->height;
}
// How many user applets have been built? Includes applets which have been inactivated by user config
uint8_t InkHUD::WindowManager::getAppletCount()
{
return userApplets.size();
}
// A tidy title for applets: used on-display in some situations
// Index is the order in the WindowManager::userApplets vector
// This is the same order that applets were added in setupNicheGraphics
const char *InkHUD::WindowManager::getAppletName(uint8_t index)
{
return userApplets.at(index)->name;
}
// Allows a system applet to prevent other applets from temporarily requesting updates
// All user applets will honor this. Some system applets might not, although they probably should
// WindowManager::forceUpdate will ignore this lock
void InkHUD::WindowManager::lock(Applet *owner)
{
// Only one system applet may lock render at once
assert(!lockOwner);
// Only system applets may lock rendering
for (Applet *a : userApplets)
assert(owner != a);
lockOwner = owner;
}
// Remove a lock placed by a system applet, which prevents other applets from rendering
void InkHUD::WindowManager::unlock(Applet *owner)
{
assert(lockOwner = owner);
lockOwner = nullptr;
// Raise this as an event (system applets only)
// - in case applet waiting for lock
// - in case applet relinquished its lock earlier, and wants it back
for (Applet *sa : systemApplets) {
// Don't raise event for the applet which is calling unlock
// - avoid loop of unlock->lock (some implementations of Applet::onLockAvailable)
if (sa != owner)
sa->onLockAvailable();
}
}
// Is an applet blocked from requesting update by a current lock?
// Applets are allowed to request updates if there is no lock, or if they are the owner of the lock
// If a == nullptr, checks permission "for everyone and anyone"
bool InkHUD::WindowManager::canRequestUpdate(Applet *a)
{
if (!lockOwner)
return true;
else if (lockOwner == a)
return true;
else
return false;
}
// Get the applet which is currently locking rendering
// We might be able to convince it release its lock, if we want it instead
InkHUD::Applet *InkHUD::WindowManager::whoLocked()
{
return WindowManager::lockOwner;
}
// Runs at regular intervals
// WindowManager's uses of this include:
// - postponing render: until next loop(), allowing all applets to be notified of some Mesh event before render
// - queuing another render: while one is already is progress
int32_t InkHUD::WindowManager::runOnce()
{
// If an applet asked to render, and hardware is able, lets try now
if (requestingUpdate && !driver->busy()) {
render();
}
// If our render() call failed, try again shortly
// otherwise, stop our thread until next update due
if (requestingUpdate)
return 250UL;
else
return OSThread::disable();
}
// Some applets may be permitted to bring themselved to foreground, to show new data
// User selects which applets have this permission via on-screen menu
// Priority is determined by the order which applets were added to WindowManager in setupNicheGraphics
// We will only autoshow one applet
void InkHUD::WindowManager::autoshow()
{
for (uint8_t i = 0; i < userApplets.size(); i++) {
Applet *a = userApplets.at(i);
if (a->wantsToAutoshow() // Applet wants to become foreground
&& !a->isForeground() // Not yet foreground
&& settings.userApplets.autoshow[i] // User permits this applet to autoshow
&& canRequestUpdate()) // Updates not currently blocked by system applet
{
Tile *t = userTiles.at(settings.userTiles.focused); // Get focused tile
t->getAssignedApplet()->sendToBackground(); // Background whichever applet is already on the tile
t->assignApplet(a); // Assign our new applet to tile
a->bringToForeground(); // Foreground our new applet
// Check if autoshown applet shows the same information as notification intended to
// In this case, we can dismiss the notification before it is shown
// Note: we are re-running the approval process. This normally occurs when the notification is initially triggered.
if (notificationApplet->isForeground() && !notificationApplet->isApproved())
notificationApplet->dismiss();
break; // One autoshow only! Avoid conflicts
}
}
}
// Check whether an update is justified
// We usually require that a foreground applet requested the update,
// but forceUpdate call will bypass these checks.
// Abstraction for WindowManager::render only
bool InkHUD::WindowManager::shouldUpdate()
{
bool should = false;
// via forceUpdate
should |= forcingUpdate;
// via user applet
for (Tile *ut : userTiles) {
Applet *ua = ut->getAssignedApplet();
if (ua // Tile has valid applet
&& ua->wantsToRender() // This applet requested display update
&& ua->isForeground() // This applet is currently shown
&& canRequestUpdate()) // Requests are not currently locked
{
should = true;
break;
}
}
// via system applet
for (Applet *sa : systemApplets) {
if (sa->wantsToRender() // This applet requested
&& sa->isForeground() // This applet is currently shown
&& canRequestUpdate(sa)) // Requests are not currently locked, or this applet owns the lock
{
should = true;
break;
}
}
return should;
}
// Determine which type of E-Ink update the display will perform, to change the image.
// Considers the needs of the various applets, then weighs against display health.
// An update type specified by forceUpdate will be granted with no further questioning.
// Abstraction for WindowManager::render only
Drivers::EInk::UpdateTypes InkHUD::WindowManager::selectUpdateType()
{
// Ask applets which update type they would prefer
// Some update types take priority over others
EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED;
if (forcingUpdate) {
// Update type was manually specified via forceUpdate
type = forcedUpdateType;
} else {
// User applets
for (Tile *ut : userTiles) {
Applet *ua = ut->getAssignedApplet();
if (ua && ua->isForeground() && canRequestUpdate())
type = mediator.prioritize(type, ua->wantsUpdateType());
}
// System Applets
for (Applet *sa : systemApplets) {
if (sa->isForeground() && canRequestUpdate(sa))
type = mediator.prioritize(type, sa->wantsUpdateType());
}
}
// Tell the mediator what update type the applets deciced on,
// find out what update type the mediator will actually allow us to have
type = mediator.evaluate(type);
return type;
}
// Run the drawing operations of any user applets which are currently displayed
// Pixel output is placed into the framebuffer, ready for handoff to the EInk driver
// Abstraction for WindowManager::render only
void InkHUD::WindowManager::renderUserApplets()
{
// Don't render any user applets if the screen is covered by a system applet using the fullscreen tile
if (fullscreenTile->getAssignedApplet())
return;
// For each tile
for (Tile *ut : userTiles) {
Applet *ua = ut->getAssignedApplet(); // Get the applet on the tile
// Don't render if tile has no applet. Handled in renderPlaceholders
if (!ua)
continue;
// Don't render the menu applet, Handled by renderSystemApplets
if (ua == menuApplet)
continue;
uint32_t start = millis();
ua->render(); // Draw!
uint32_t stop = millis();
LOG_DEBUG("%s took %dms to render", ua->name, stop - start);
}
}
// Run the drawing operations of any system applets which are currently displayed
// Pixel output is placed into the framebuffer, ready for handoff to the EInk driver
// Abstraction for WindowManager::render only
void InkHUD::WindowManager::renderSystemApplets()
{
// Each system applet
for (Applet *sa : systemApplets) {
// Skip if not shown
if (!sa->isForeground())
continue;
// Don't draw the battery overtop the menu
// Todo: smarter way to handle this
if (sa == batteryIconApplet && menuApplet->isForeground())
continue;
// Skip applet if fullscreen tile is in use, but not used by this applet
// Applet is "obscured"
if (fullscreenTile->getAssignedApplet() && fullscreenTile->getAssignedApplet() != sa)
continue;
// uint32_t start = millis(); // Debugging only: runtime
sa->render(); // Draw!
// uint32_t stop = millis(); // Debugging only: runtime
// LOG_DEBUG("%s (system) took %dms to render", (sa->name == nullptr) ? "Unnamed" : sa->name, stop - start);
}
}
// In some situations (e.g. layout or applet selection changes),
// a user tile can end up without an assigned applet.
// In this case, we will fill the empty space with diagonal lines.
void InkHUD::WindowManager::renderPlaceholders()
{
// Don't draw if obscured by the fullscreen tile
if (fullscreenTile->getAssignedApplet())
return;
for (Tile *ut : userTiles) {
// If no applet assigned
if (!ut->getAssignedApplet()) {
ut->assignApplet(placeholderApplet);
placeholderApplet->render();
ut->assignApplet(nullptr);
}
}
}
// Make an attempt to gather image data from some / all applets, and update the display
// Might not be possible right now, if update already is progress.
void InkHUD::WindowManager::render(bool async)
{
// Make sure the display is ready for a new update
if (async) {
// Previous update still running, Will try again shortly, via runOnce()
if (driver->busy())
return;
} else {
// Wait here for previous update to complete
driver->await();
}
// (Potentially) change applet to display new info,
// then check if this newly displayed applet makes a pending notification redundant
autoshow();
// If an update is justified.
// We don't know this until after autoshow has run, as new applets may now be in foreground
if (shouldUpdate()) {
// Decide which technique the display will use to change image
EInk::UpdateTypes updateType = selectUpdateType();
// Render the new image
clearBuffer();
renderUserApplets();
renderSystemApplets();
renderPlaceholders();
// Tell display to begin process of drawing new image
LOG_INFO("Updating display");
driver->update(imageBuffer, updateType);
// If not async, wait here until the update is complete
if (!async)
driver->await();
} else
LOG_DEBUG("Not updating display");
// Our part is done now.
// If update is async, the display hardware is still performing the update process,
// but that's all handled by NicheGraphics::Drivers::EInk
// Tidy up, ready for a new request
requestingUpdate = false;
forcingUpdate = false;
forcedUpdateType = EInk::UpdateTypes::UNSPECIFIED;
}
// Set a ready-to-draw pixel into the image buffer
// All rotations / translations have already taken place: this buffer data is formatted ready for the driver
void InkHUD::WindowManager::setBufferPixel(int16_t x, int16_t y, Color c)
{
uint32_t byteNum = (y * imageBufferWidth) + (x / 8); // X data is 8 pixels per byte
uint8_t bitNum = 7 - (x % 8); // Invert order: leftmost bit (most significant) is leftmost pixel of byte.
bitWrite(imageBuffer[byteNum], bitNum, c);
}
// Applies the system-wide rotation to pixel positions
// This step is applied to image data which has already been translated by a Tile object
// This is the final step before the pixel is placed into the image buffer
// No return: values of the *x and *y parameters are modified by the method
void InkHUD::WindowManager::rotatePixelCoords(int16_t *x, int16_t *y)
{
// Apply a global rotation to pixel locations
int16_t x1 = 0;
int16_t y1 = 0;
switch (settings.rotation) {
case 0:
x1 = *x;
y1 = *y;
break;
case 1:
x1 = (driver->width - 1) - *y;
y1 = *x;
break;
case 2:
x1 = (driver->width - 1) - *x;
y1 = (driver->height - 1) - *y;
break;
case 3:
x1 = *y;
y1 = (driver->height - 1) - *x;
break;
}
*x = x1;
*y = y1;
}
// Manually fill the image buffer with WHITE
// Clears any old drawing
void InkHUD::WindowManager::clearBuffer()
{
memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth);
}
// Seach for any applets which believe they are foreground, but no longer have a valid tile
// Tidies up after layout changes at runtime
void InkHUD::WindowManager::findOrphanApplets()
{
for (uint8_t ia = 0; ia < userApplets.size(); ia++) {
Applet *a = userApplets.at(ia);
// Applet doesn't believe it is displayed: not orphaned
if (!a->isForeground())
continue;
// Check each tile, to see if anyone claims this applet
bool foundOwner = false;
for (uint8_t it = 0; it < userTiles.size(); it++) {
Tile *t = userTiles.at(it);
// A tile claims this applet: not orphaned
if (t->getAssignedApplet() == a) {
foundOwner = true;
break;
}
}
// Orphan found
// Tell the applet that no tile is currently displaying it
// This allows the focussed tile to cycle to this applet again by pressing user button
if (!foundOwner)
a->sendToBackground();
}
}
#endif