kopia lustrzana https://github.com/meshtastic/firmware
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](pull/6184/head^231ee3d90c8...37245b3d61
) --- 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 commit9f8d86cb25
. * 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>
rodzic
088fce7d11
commit
99d3e5eb70
|
@ -1,6 +1,9 @@
|
|||
[submodule "protobufs"]
|
||||
path = protobufs
|
||||
url = https://github.com/meshtastic/protobufs.git
|
||||
[submodule "lib/device-ui"]
|
||||
path = lib/device-ui
|
||||
url = https://github.com/meshtastic/device-ui.git
|
||||
[submodule "meshtestic"]
|
||||
path = meshtestic
|
||||
url = https://github.com/meshtastic/meshTestic
|
||||
|
|
|
@ -37,6 +37,7 @@ build_flags =
|
|||
-DLIBPAX_ARDUINO
|
||||
-DLIBPAX_WIFI
|
||||
-DLIBPAX_BLE
|
||||
-DHAS_UDP_MULTICAST=1
|
||||
;-DDEBUG_HEAP
|
||||
|
||||
lib_deps =
|
||||
|
|
|
@ -18,6 +18,7 @@ build_src_filter =
|
|||
|
||||
lib_ignore =
|
||||
BluetoothOTA
|
||||
lvgl
|
||||
|
||||
lib_deps =
|
||||
${arduino_base.lib_deps}
|
||||
|
|
|
@ -35,11 +35,11 @@ cp $SRCBIN $OUTDIR/$basename-update.bin
|
|||
|
||||
echo "Building Filesystem for ESP32 targets"
|
||||
pio run --environment $1 -t buildfs
|
||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$VERSION.bin
|
||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin
|
||||
# Remove webserver files from the filesystem and rebuild
|
||||
ls -l data/static # Diagnostic list of files
|
||||
rm -rf data/static
|
||||
pio run --environment $1 -t buildfs
|
||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$VERSION.bin
|
||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$1-$VERSION.bin
|
||||
cp bin/device-install.* $OUTDIR
|
||||
cp bin/device-update.* $OUTDIR
|
||||
cp bin/device-update.* $OUTDIR
|
|
@ -0,0 +1,4 @@
|
|||
Display:
|
||||
Panel: X11
|
||||
Width: 480
|
||||
Height: 480
|
|
@ -35,6 +35,11 @@ for subdir, dirs, files in os.walk(rootdir):
|
|||
outlist.append(section)
|
||||
else:
|
||||
outlist.append(section)
|
||||
# Add the TFT variants if the base variant is selected
|
||||
elif section.replace("-tft", "") in outlist and config[config[c].name].get("board_level") != "extra":
|
||||
outlist.append(section)
|
||||
elif section.replace("-inkhud", "") in outlist and config[config[c].name].get("board_level") != "extra":
|
||||
outlist.append(section)
|
||||
if "board_check" in config[config[c].name]:
|
||||
if (config[config[c].name]["board_check"] == "true") & (
|
||||
"check" in options
|
||||
|
@ -43,4 +48,4 @@ for subdir, dirs, files in os.walk(rootdir):
|
|||
if ("quick" in options) & (len(outlist) > 3):
|
||||
print(json.dumps(random.sample(outlist, 3)))
|
||||
else:
|
||||
print(json.dumps(outlist))
|
||||
print(json.dumps(outlist))
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 5c6156d2aa10d62cca3e57ffc117b934ef2fbffe
|
|
@ -0,0 +1,105 @@
|
|||
#pragma once
|
||||
#include "Status.h"
|
||||
#include "assert.h"
|
||||
#include "configuration.h"
|
||||
#include "meshUtils.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace meshtastic
|
||||
{
|
||||
|
||||
// Describes the state of the Bluetooth connection
|
||||
// Allows display to handle pairing events without each UI needing to explicitly hook the Bluefruit / NimBLE code
|
||||
class BluetoothStatus : public Status
|
||||
{
|
||||
public:
|
||||
enum class ConnectionState {
|
||||
DISCONNECTED,
|
||||
PAIRING,
|
||||
CONNECTED,
|
||||
};
|
||||
|
||||
private:
|
||||
CallbackObserver<BluetoothStatus, const BluetoothStatus *> statusObserver =
|
||||
CallbackObserver<BluetoothStatus, const BluetoothStatus *>(this, &BluetoothStatus::updateStatus);
|
||||
|
||||
ConnectionState state = ConnectionState::DISCONNECTED;
|
||||
std::string passkey; // Stored as string, because Bluefruit allows passkeys with a leading zero
|
||||
|
||||
public:
|
||||
BluetoothStatus() { statusType = STATUS_TYPE_BLUETOOTH; }
|
||||
|
||||
// New BluetoothStatus: connected or disconnected
|
||||
BluetoothStatus(ConnectionState state)
|
||||
{
|
||||
assert(state != ConnectionState::PAIRING); // If pairing, use constructor which specifies passkey
|
||||
statusType = STATUS_TYPE_BLUETOOTH;
|
||||
this->state = state;
|
||||
}
|
||||
|
||||
// New BluetoothStatus: pairing, with passkey
|
||||
BluetoothStatus(std::string passkey) : Status()
|
||||
{
|
||||
statusType = STATUS_TYPE_BLUETOOTH;
|
||||
this->state = ConnectionState::PAIRING;
|
||||
this->passkey = passkey;
|
||||
}
|
||||
|
||||
ConnectionState getConnectionState() const { return this->state; }
|
||||
|
||||
std::string getPasskey() const
|
||||
{
|
||||
assert(state == ConnectionState::PAIRING);
|
||||
return this->passkey;
|
||||
}
|
||||
|
||||
void observe(Observable<const BluetoothStatus *> *source) { statusObserver.observe(source); }
|
||||
|
||||
bool matches(const BluetoothStatus *newStatus) const
|
||||
{
|
||||
if (this->state == newStatus->getConnectionState()) {
|
||||
// Same state: CONNECTED / DISCONNECTED
|
||||
if (this->state != ConnectionState::PAIRING)
|
||||
return true;
|
||||
// Same state: PAIRING, and passkey matches
|
||||
else if (this->getPasskey() == newStatus->getPasskey())
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int updateStatus(const BluetoothStatus *newStatus)
|
||||
{
|
||||
// Has the status changed?
|
||||
if (!matches(newStatus)) {
|
||||
// Copy the members
|
||||
state = newStatus->getConnectionState();
|
||||
if (state == ConnectionState::PAIRING)
|
||||
passkey = newStatus->getPasskey();
|
||||
|
||||
// Tell anyone interested that we have an update
|
||||
onNewStatus.notifyObservers(this);
|
||||
|
||||
// Debug only:
|
||||
switch (state) {
|
||||
case ConnectionState::PAIRING:
|
||||
LOG_DEBUG("BluetoothStatus PAIRING, key=%s", passkey.c_str());
|
||||
break;
|
||||
case ConnectionState::CONNECTED:
|
||||
LOG_DEBUG("BluetoothStatus CONNECTED");
|
||||
break;
|
||||
|
||||
case ConnectionState::DISCONNECTED:
|
||||
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace meshtastic
|
||||
|
||||
extern meshtastic::BluetoothStatus *bluetoothStatus;
|
|
@ -11,6 +11,7 @@
|
|||
#include "main.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#include "power.h"
|
||||
#include "sleep.h"
|
||||
#ifdef ARCH_PORTDUINO
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
#endif
|
||||
|
@ -99,6 +100,13 @@ ButtonThread::ButtonThread() : OSThread("Button")
|
|||
userButtonTouch.attachLongPressStart(touchPressedLongStart); // Better handling with longpress than click?
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Register callbacks for before and after lightsleep
|
||||
// Used to detach and reattach interrupts
|
||||
lsObserver.observe(¬ifyLightSleep);
|
||||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||
#endif
|
||||
|
||||
attachButtonInterrupts();
|
||||
#endif
|
||||
}
|
||||
|
@ -320,6 +328,26 @@ void ButtonThread::detachButtonInterrupts()
|
|||
#endif
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
|
||||
// Detach our class' interrupts before lightsleep
|
||||
// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press
|
||||
int ButtonThread::beforeLightSleep(void *unused)
|
||||
{
|
||||
detachButtonInterrupts();
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
// Reconfigure our interrupts
|
||||
// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep
|
||||
int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause)
|
||||
{
|
||||
attachButtonInterrupts();
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Watch a GPIO and if we get an IRQ, wake the main thread.
|
||||
* Use to add wake on button press
|
||||
|
|
|
@ -37,6 +37,12 @@ class ButtonThread : public concurrency::OSThread
|
|||
void detachButtonInterrupts();
|
||||
void storeClickCount();
|
||||
|
||||
// Disconnect and reconnect interrupts for light sleep
|
||||
#ifdef ARCH_ESP32
|
||||
int beforeLightSleep(void *unused);
|
||||
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
|
||||
#endif
|
||||
|
||||
private:
|
||||
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN)
|
||||
static OneButton userButton; // Static - accessed from an interrupt
|
||||
|
@ -48,6 +54,14 @@ class ButtonThread : public concurrency::OSThread
|
|||
OneButton userButtonTouch;
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Get notified when lightsleep begins and ends
|
||||
CallbackObserver<ButtonThread, void *> lsObserver =
|
||||
CallbackObserver<ButtonThread, void *>(this, &ButtonThread::beforeLightSleep);
|
||||
CallbackObserver<ButtonThread, esp_sleep_wakeup_cause_t> lsEndObserver =
|
||||
CallbackObserver<ButtonThread, esp_sleep_wakeup_cause_t>(this, &ButtonThread::afterLightSleep);
|
||||
#endif
|
||||
|
||||
// set during IRQ
|
||||
static volatile ButtonEventType btnEvent;
|
||||
|
||||
|
|
|
@ -23,6 +23,10 @@ SPIClass SPI1(HSPI);
|
|||
#define SDHandler SPI
|
||||
#endif
|
||||
|
||||
#ifndef SD_SPI_FREQUENCY
|
||||
#define SD_SPI_FREQUENCY 4000000U
|
||||
#endif
|
||||
|
||||
#endif // HAS_SDCARD
|
||||
|
||||
#if defined(ARCH_STM32WL)
|
||||
|
@ -361,8 +365,7 @@ void setupSDCard()
|
|||
#ifdef HAS_SDCARD
|
||||
concurrency::LockGuard g(spiLock);
|
||||
SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI);
|
||||
|
||||
if (!SD.begin(SDCARD_CS, SDHandler)) {
|
||||
if (!SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) {
|
||||
LOG_DEBUG("No SD_MMC card detected");
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#define STATUS_TYPE_POWER 1
|
||||
#define STATUS_TYPE_GPS 2
|
||||
#define STATUS_TYPE_NODE 3
|
||||
#define STATUS_TYPE_BLUETOOTH 4
|
||||
|
||||
namespace meshtastic
|
||||
{
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include "./LatchingBacklight.h"
|
||||
|
||||
#include "assert.h"
|
||||
|
||||
#include "sleep.h"
|
||||
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Private constructor
|
||||
// Called by getInstance
|
||||
LatchingBacklight::LatchingBacklight()
|
||||
{
|
||||
// Attach the deep sleep callback
|
||||
deepSleepObserver.observe(¬ifyDeepSleep);
|
||||
}
|
||||
|
||||
// Get access to (or create) the singleton instance of this class
|
||||
LatchingBacklight *LatchingBacklight::getInstance()
|
||||
{
|
||||
// Instantiate the class the first time this method is called
|
||||
static LatchingBacklight *const singletonInstance = new LatchingBacklight;
|
||||
|
||||
return singletonInstance;
|
||||
}
|
||||
|
||||
// Which pin controls the backlight?
|
||||
// Is the light active HIGH (default) or active LOW?
|
||||
void LatchingBacklight::setPin(uint8_t pin, bool activeWhen)
|
||||
{
|
||||
this->pin = pin;
|
||||
this->logicActive = activeWhen;
|
||||
|
||||
pinMode(pin, OUTPUT);
|
||||
off(); // Explicit off seem required by T-Echo?
|
||||
}
|
||||
|
||||
// Called when device is shutting down
|
||||
// Ensures the backlight is off
|
||||
int LatchingBacklight::beforeDeepSleep(void *unused)
|
||||
{
|
||||
// We shouldn't need to guard the block like this
|
||||
// Contingency for:
|
||||
// - settings corruption: settings.optionalMenuItems.backlight guards backlight code in MenuApplet
|
||||
// - improper use in the future
|
||||
if (pin != (uint8_t)-1) {
|
||||
off();
|
||||
pinMode(pin, INPUT); // High impedence - unnecessary?
|
||||
} else
|
||||
LOG_WARN("LatchingBacklight instantiated, but pin not set");
|
||||
return 0; // Continue with deep sleep
|
||||
}
|
||||
|
||||
// Turn the backlight on *temporarily*
|
||||
// This should be used for momentary illumination, such as while a button is held
|
||||
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
|
||||
void LatchingBacklight::peek()
|
||||
{
|
||||
assert(pin != (uint8_t)-1);
|
||||
digitalWrite(pin, logicActive); // On
|
||||
on = true;
|
||||
latched = false;
|
||||
}
|
||||
|
||||
// Turn the backlight on, and keep it on
|
||||
// This should be used when the backlight should remain active, even after user input ends
|
||||
// e.g. when enabled via the menu
|
||||
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
|
||||
void LatchingBacklight::latch()
|
||||
{
|
||||
assert(pin != (uint8_t)-1);
|
||||
|
||||
// Blink if moving from peek to latch
|
||||
// Indicates to user that the transition has taken place
|
||||
if (on && !latched) {
|
||||
digitalWrite(pin, !logicActive); // Off
|
||||
delay(25);
|
||||
digitalWrite(pin, logicActive); // On
|
||||
delay(25);
|
||||
digitalWrite(pin, !logicActive); // Off
|
||||
delay(25);
|
||||
}
|
||||
|
||||
digitalWrite(pin, logicActive); // On
|
||||
on = true;
|
||||
latched = true;
|
||||
}
|
||||
|
||||
// Turn the backlight off
|
||||
// Suitable for ending both peek and latch
|
||||
void LatchingBacklight::off()
|
||||
{
|
||||
assert(pin != (uint8_t)-1);
|
||||
digitalWrite(pin, !logicActive); // Off
|
||||
on = false;
|
||||
latched = false;
|
||||
}
|
||||
|
||||
bool LatchingBacklight::isOn()
|
||||
{
|
||||
return on;
|
||||
}
|
||||
|
||||
bool LatchingBacklight::isLatched()
|
||||
{
|
||||
return latched;
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
|
||||
Singleton class
|
||||
On-demand control of a display's backlight, connected to a GPIO
|
||||
Initial use case is control of T-Echo's frontlight, via the capacitive touch button
|
||||
|
||||
- momentary on
|
||||
- latched on
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "Observer.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
|
||||
class LatchingBacklight
|
||||
{
|
||||
public:
|
||||
static LatchingBacklight *getInstance(); // Create or get the singleton instance
|
||||
void setPin(uint8_t pin, bool activeWhen = HIGH);
|
||||
|
||||
int beforeDeepSleep(void *unused); // Callback for auto-shutoff
|
||||
|
||||
void peek(); // Backlight on temporarily, e.g. while button held
|
||||
void latch(); // Backlight on permanently, e.g. toggled via menu
|
||||
void off(); // Backlight off. Suitable for both peek and latch
|
||||
|
||||
bool isOn(); // Either peek or latch
|
||||
bool isLatched();
|
||||
|
||||
private:
|
||||
LatchingBacklight(); // Constructor made private: force use of getInstance
|
||||
|
||||
// Get notified when the system is shutting down
|
||||
CallbackObserver<LatchingBacklight, void *> deepSleepObserver =
|
||||
CallbackObserver<LatchingBacklight, void *>(this, &LatchingBacklight::beforeDeepSleep);
|
||||
|
||||
uint8_t pin = (uint8_t)-1;
|
||||
bool logicActive = HIGH; // Is light active HIGH or active LOW
|
||||
|
||||
bool on = false; // Is light on (either peek or latched)
|
||||
bool latched = false; // Is light latched on
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
|
@ -0,0 +1 @@
|
|||
#include "./DEPG0154BNS800.h"
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
|
||||
E-Ink display driver
|
||||
- DEPG0154BNS800
|
||||
- Manufacturer: DKE
|
||||
- Size: 1.54 inch
|
||||
- Resolution: 152px x 152px
|
||||
- Flex connector marking: FPC7525
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./SSD16XX.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
class DEPG0154BNS800 : public SSD16XX
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 152;
|
||||
static constexpr uint32_t height = 152;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL);
|
||||
|
||||
public:
|
||||
DEPG0154BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
|
@ -0,0 +1,120 @@
|
|||
#include "./DEPG0290BNS800.h"
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Describes the operation performed when a "fast refresh" is performed
|
||||
// Source: custom, with DEPG0150BNS810 as a reference
|
||||
static const uint8_t LUT_FAST[] = {
|
||||
// 1 2 3 4
|
||||
0x40, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2B (Existing black pixels)
|
||||
0x00, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2W (New white pixels)
|
||||
0x00, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2B (New black pixels)
|
||||
0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2W (Existing white pixels)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VCOM
|
||||
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 1. Tap existing black pixels back into place
|
||||
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2. Move new pixels
|
||||
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 3. New pixels, and also existing black pixels
|
||||
0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // 4. All pixels, then cooldown
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
|
||||
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
// How strongly the pixels are pulled and pushed
|
||||
void DEPG0290BNS800::configVoltages()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
// Listed as "typical" in datasheet
|
||||
sendCommand(0x04);
|
||||
sendData(0x41); // VSH1 15V
|
||||
sendData(0x00); // VSH2 NA
|
||||
sendData(0x32); // VSL -15V
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
// From OTP memory
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Load settings about how the pixels are moved from old state to new state during a refresh
|
||||
// - manually specified,
|
||||
// - or with stored values from displays OTP memory
|
||||
void DEPG0290BNS800::configWaveform()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x60); // Actively hold screen border during update
|
||||
|
||||
sendCommand(0x32); // Write LUT register from MCU:
|
||||
sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh)
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
// From OTP memory
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Describes the sequence of events performed by the displays controller IC during a refresh
|
||||
// Includes "power up", "load settings from memory", "update the pixels", etc
|
||||
void DEPG0290BNS800::configUpdateSequence()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xCF); // Differential, use manually loaded waveform
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Non-differential, load waveform from OTP
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Once the refresh operation has been started,
|
||||
// begin periodically polling the display to check for completion, using the normal Meshtastic threading code
|
||||
// Only used when refresh is "async"
|
||||
void DEPG0290BNS800::detachFromUpdate()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 450); // At least 450ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 3000); // At least 3 seconds for full refresh
|
||||
}
|
||||
}
|
||||
|
||||
// For this display, we do not need to re-write the new image.
|
||||
// We're overriding SSD16XX::finalizeUpdate to make this small optimization.
|
||||
// The display does also work just fine with the generic SSD16XX method, though.
|
||||
void DEPG0290BNS800::finalizeUpdate()
|
||||
{
|
||||
// Put a copy of the image into the "old memory".
|
||||
// Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place
|
||||
// We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc.
|
||||
if (updateType != FULL) {
|
||||
// writeNewImage(); // Not required for this display
|
||||
writeOldImage();
|
||||
sendCommand(0x7F); // Terminate image write without update
|
||||
wait();
|
||||
}
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
|
||||
E-Ink display driver
|
||||
- DEPG0290BNS800
|
||||
- Manufacturer: DKE
|
||||
- Size: 2.9 inch
|
||||
- Resolution: 128px x 296px
|
||||
- Flex connector marking: FPC-7519 rev.b
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./SSD16XX.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
class DEPG0290BNS800 : public SSD16XX
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 128;
|
||||
static constexpr uint32_t height = 296;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
|
||||
public:
|
||||
DEPG0290BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte
|
||||
|
||||
protected:
|
||||
void configVoltages() override;
|
||||
void configWaveform() override;
|
||||
void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
void finalizeUpdate() override; // Only overriden for a slight optimization
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
|
@ -0,0 +1,70 @@
|
|||
#include "./EInk.h"
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Separate from EInk::begin method, as derived class constructors can probably supply these parameters as constants
|
||||
EInk::EInk(uint16_t width, uint16_t height, UpdateTypes supported)
|
||||
: concurrency::OSThread("E-Ink Driver"), width(width), height(height), supportedUpdateTypes(supported)
|
||||
{
|
||||
OSThread::disable();
|
||||
}
|
||||
|
||||
// Used by NicheGraphics implementations to check if a display supports a specific refresh operation.
|
||||
// Whether or the update type is supported is specified in the constructor
|
||||
bool EInk::supports(UpdateTypes type)
|
||||
{
|
||||
// The EInkUpdateTypes enum assigns each type a unique bit. We are checking if that bit is set.
|
||||
if (supportedUpdateTypes & type)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
// Begins using the OSThread to detect when a display update is complete
|
||||
// This allows the refresh operation to run "asynchronously".
|
||||
// Rather than blocking execution waiting for the update to complete, we are periodically checking the hardware's BUSY pin
|
||||
// The expectedDuration argument allows us to delay the start of this checking, if we know "roughly" how long an update takes.
|
||||
// Potentially, a display without hardware BUSY could rely entirely on "expectedDuration",
|
||||
// provided its isUpdateDone() override always returns true.
|
||||
void EInk::beginPolling(uint32_t interval, uint32_t expectedDuration)
|
||||
{
|
||||
updateRunning = true;
|
||||
updateBegunAt = millis();
|
||||
pollingInterval = interval;
|
||||
|
||||
// To minimize load, we can choose to delay polling for a few seconds, if we know roughly how long the update will take
|
||||
// By default, expectedDuration is 0, and we'll start polling immediately
|
||||
OSThread::setIntervalFromNow(expectedDuration);
|
||||
OSThread::enabled = true;
|
||||
}
|
||||
|
||||
// Meshtastic's pseudo-threading layer
|
||||
// We're using this as a timer, to periodically check if an update is complete
|
||||
// This is what allows us to update the display asynchronously
|
||||
int32_t EInk::runOnce()
|
||||
{
|
||||
if (!isUpdateDone())
|
||||
return pollingInterval; // Poll again in a few ms
|
||||
|
||||
// If update done:
|
||||
finalizeUpdate(); // Any post-update code: power down panel hardware, hibernate, etc
|
||||
updateRunning = false; // Change what we report via EInk::busy()
|
||||
return disable(); // Stop polling
|
||||
}
|
||||
|
||||
// Wait for an in progress update to complete before continuing
|
||||
// Run a normal (async) update first, *then* call await
|
||||
void EInk::await()
|
||||
{
|
||||
// Stop our concurrency thread
|
||||
OSThread::disable();
|
||||
|
||||
// Sit and block until the update is complete
|
||||
while (updateRunning) {
|
||||
runOnce();
|
||||
yield();
|
||||
}
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
|
||||
Base class for E-Ink display drivers
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#include "configuration.h"
|
||||
|
||||
#include "concurrency/OSThread.h"
|
||||
#include <SPI.h>
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
|
||||
class EInk : private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
// Different possible operations used to update an E-Ink display
|
||||
// Some displays will not support all operations
|
||||
// Each value needs a unique bit. In some cases, we might set more than one bit (e.g. EInk::supportedUpdateType)
|
||||
enum UpdateTypes : uint8_t {
|
||||
UNSPECIFIED = 0,
|
||||
FULL = 1 << 0,
|
||||
FAST = 1 << 1,
|
||||
};
|
||||
|
||||
EInk(uint16_t width, uint16_t height, UpdateTypes supported);
|
||||
virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) = 0;
|
||||
virtual void update(uint8_t *imageData, UpdateTypes type) = 0; // Change the display image
|
||||
void await(); // Wait for an in-progress update to complete before proceeding
|
||||
bool supports(UpdateTypes type); // Can display perfom a certain update type
|
||||
bool busy() { return updateRunning; } // Display able to update right now?
|
||||
|
||||
const uint16_t width; // Public so that NicheGraphics implementations can access. Safe because const.
|
||||
const uint16_t height;
|
||||
|
||||
protected:
|
||||
void beginPolling(uint32_t interval, uint32_t expectedDuration); // Begin checking repeatedly if update finished
|
||||
virtual bool isUpdateDone() = 0; // Check once if update finished
|
||||
virtual void finalizeUpdate() {} // Run any post-update code
|
||||
|
||||
private:
|
||||
int32_t runOnce() override; // Repeated checking if update finished
|
||||
|
||||
const UpdateTypes supportedUpdateTypes; // Capabilities of a derived display class
|
||||
bool updateRunning = false; // see EInk::busy()
|
||||
uint32_t updateBegunAt; // For initial pause before polling for update completion
|
||||
uint32_t pollingInterval; // How often to check if update complete (ms)
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
|
@ -0,0 +1,61 @@
|
|||
#include "./GDEY0154D67.h"
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Map the display controller IC's output to the conected panel
|
||||
void GDEY0154D67::configScanning()
|
||||
{
|
||||
// "Driver output control"
|
||||
sendCommand(0x01);
|
||||
sendData(0xC7);
|
||||
sendData(0x00);
|
||||
sendData(0x00);
|
||||
|
||||
// To-do: delete this method?
|
||||
// Values set here might be redundant: C7, 00, 00 seems to be default
|
||||
}
|
||||
|
||||
// Specify which information is used to control the sequence of voltages applied to move the pixels
|
||||
// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from
|
||||
// the controller IC's OTP memory, when the update procedure begins.
|
||||
void GDEY0154D67::configWaveform()
|
||||
{
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white)
|
||||
|
||||
sendCommand(0x18); // Temperature sensor:
|
||||
sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
|
||||
}
|
||||
|
||||
void GDEY0154D67::configUpdateSequence()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Once the refresh operation has been started,
|
||||
// begin periodically polling the display to check for completion, using the normal Meshtastic threading code
|
||||
// Only used when refresh is "async"
|
||||
void GDEY0154D67::detachFromUpdate()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 500); // At least 500ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 2000); // At least 2 seconds for full refresh
|
||||
}
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
|
||||
E-Ink display driver
|
||||
- GDEY0154D67
|
||||
- Manufacturer: Goodisplay
|
||||
- Size: 1.54 inch
|
||||
- Resolution: 200px x 200px
|
||||
- Flex connector marking: FPC-B001
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./SSD16XX.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
class GDEY0154D67 : public SSD16XX
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 200;
|
||||
static constexpr uint32_t height = 200;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
|
||||
public:
|
||||
GDEY0154D67() : SSD16XX(width, height, supported) {}
|
||||
|
||||
protected:
|
||||
virtual void configScanning() override;
|
||||
virtual void configWaveform() override;
|
||||
virtual void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
|
@ -0,0 +1,301 @@
|
|||
#include "./LCMEN2R13EFC1.h"
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Look up table: fast refresh, common electrode
|
||||
static const uint8_t LUT_FAST_VCOMDC[] = {
|
||||
0x01, 0x06, 0x03, 0x02, 0x01, 0x01, 0x01, //
|
||||
0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
};
|
||||
|
||||
// Look up table: fast refresh, pixels which remain white
|
||||
static const uint8_t LUT_FAST_WW[] = {
|
||||
0x01, 0x06, 0x03, 0x02, 0x81, 0x01, 0x01, //
|
||||
0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
};
|
||||
|
||||
// Look up table: fast refresh, pixel which change from black to white
|
||||
static const uint8_t LUT_FAST_BW[] = {
|
||||
0x01, 0x86, 0x83, 0x82, 0x81, 0x01, 0x01, //
|
||||
0x01, 0x86, 0x82, 0x01, 0x01, 0x01, 0x01, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
};
|
||||
|
||||
// Look up table: fash refresh, pixels which change from white to black
|
||||
static const uint8_t LUT_FAST_WB[] = {
|
||||
0x01, 0x46, 0x42, 0x01, 0x01, 0x01, 0x01, //
|
||||
0x01, 0x46, 0x42, 0x01, 0x01, 0x01, 0x01, //
|
||||
0x01, 0x46, 0x43, 0x02, 0x01, 0x01, 0x01, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
};
|
||||
|
||||
// Look up table: fash refresh, pixels which remain black
|
||||
static const uint8_t LUT_FAST_BB[] = {
|
||||
0x01, 0x06, 0x03, 0x42, 0x41, 0x01, 0x01, //
|
||||
0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
};
|
||||
|
||||
LCMEN213EFC1::LCMEN213EFC1() : EInk(width, height, supported)
|
||||
{
|
||||
// Pre-calculate size of the image buffer, for convenience
|
||||
|
||||
// Determine the X dimension 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.
|
||||
bufferRowSize = ((width - 1) / 8) + 1;
|
||||
|
||||
// Total size of image buffer, in bytes.
|
||||
bufferSize = bufferRowSize * height;
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
|
||||
{
|
||||
this->spi = spi;
|
||||
this->pin_dc = pin_dc;
|
||||
this->pin_cs = pin_cs;
|
||||
this->pin_busy = pin_busy;
|
||||
this->pin_rst = pin_rst;
|
||||
|
||||
pinMode(pin_dc, OUTPUT);
|
||||
pinMode(pin_cs, OUTPUT);
|
||||
pinMode(pin_busy, INPUT);
|
||||
|
||||
// Reset is active low, hold high
|
||||
pinMode(pin_rst, INPUT_PULLUP);
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::update(uint8_t *imageData, UpdateTypes type)
|
||||
{
|
||||
this->updateType = type;
|
||||
this->buffer = imageData;
|
||||
|
||||
reset();
|
||||
|
||||
// Config
|
||||
if (updateType == FULL)
|
||||
configFull();
|
||||
else
|
||||
configFast();
|
||||
|
||||
// Transfer image data
|
||||
if (updateType == FULL) {
|
||||
writeNewImage();
|
||||
writeOldImage();
|
||||
} else {
|
||||
writeNewImage();
|
||||
}
|
||||
|
||||
sendCommand(0x04); // Power on the panel voltage
|
||||
wait();
|
||||
|
||||
sendCommand(0x12); // Begin executing the update
|
||||
|
||||
// Let the update run async, on display hardware. Base class will poll completion, then finalize.
|
||||
// For a blocking update, call await after update
|
||||
detachFromUpdate();
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::wait()
|
||||
{
|
||||
// Busy when LOW
|
||||
while (digitalRead(pin_busy) == LOW)
|
||||
yield();
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::reset()
|
||||
{
|
||||
pinMode(pin_rst, OUTPUT);
|
||||
digitalWrite(pin_rst, LOW);
|
||||
delay(10);
|
||||
pinMode(pin_rst, INPUT_PULLUP);
|
||||
wait();
|
||||
|
||||
sendCommand(0x12);
|
||||
wait();
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::sendCommand(const uint8_t command)
|
||||
{
|
||||
spi->beginTransaction(spiSettings);
|
||||
digitalWrite(pin_dc, LOW); // DC pin low indicates command
|
||||
digitalWrite(pin_cs, LOW);
|
||||
spi->transfer(command);
|
||||
digitalWrite(pin_cs, HIGH);
|
||||
digitalWrite(pin_dc, HIGH);
|
||||
spi->endTransaction();
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::sendData(uint8_t data)
|
||||
{
|
||||
// spi->beginTransaction(spiSettings);
|
||||
// digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command
|
||||
// digitalWrite(pin_cs, LOW);
|
||||
// spi->transfer(data);
|
||||
// digitalWrite(pin_cs, HIGH);
|
||||
// digitalWrite(pin_dc, HIGH);
|
||||
// spi->endTransaction();
|
||||
sendData(&data, 1);
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::sendData(const uint8_t *data, uint32_t size)
|
||||
{
|
||||
spi->beginTransaction(spiSettings);
|
||||
digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command
|
||||
digitalWrite(pin_cs, LOW);
|
||||
|
||||
// Platform-specific SPI command
|
||||
// Mothballing. This display model is only used by Heltec Wireless Paper (ESP32)
|
||||
#if defined(ARCH_ESP32)
|
||||
spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer
|
||||
#elif defined(ARCH_NRF52)
|
||||
spi->transfer(data, NULL, size); // NULL for a "write only" transfer
|
||||
#else
|
||||
#error Not implemented yet? Feel free to add other platforms here.
|
||||
#endif
|
||||
|
||||
digitalWrite(pin_cs, HIGH);
|
||||
digitalWrite(pin_dc, HIGH);
|
||||
spi->endTransaction();
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::configFull()
|
||||
{
|
||||
sendCommand(0x00); // Panel setting register
|
||||
sendData(0b11 << 6 // Display resolution
|
||||
| 1 << 4 // B&W only
|
||||
| 1 << 3 // Vertical scan direction
|
||||
| 1 << 2 // Horizontal scan direction
|
||||
| 1 << 1 // Shutdown: no
|
||||
| 1 << 0 // Reset: no
|
||||
);
|
||||
|
||||
sendCommand(0x50); // VCOM and data interval setting register
|
||||
sendData(0b10 << 6 // Border driven white
|
||||
| 0b11 << 4 // Invert image colors: no
|
||||
| 0b0111 << 0 // Interval between VCOM on and image data (default)
|
||||
);
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::configFast()
|
||||
{
|
||||
sendCommand(0x00); // Panel setting register
|
||||
sendData(0b11 << 6 // Display resolution
|
||||
| 1 << 5 // LUT from registers (set below)
|
||||
| 1 << 4 // B&W only
|
||||
| 1 << 3 // Vertical scan direction
|
||||
| 1 << 2 // Horizontal scan direction
|
||||
| 1 << 1 // Shutdown: no
|
||||
| 1 << 0 // Reset: no
|
||||
);
|
||||
|
||||
sendCommand(0x50); // VCOM and data interval setting register
|
||||
sendData(0b11 << 6 // Border floating
|
||||
| 0b01 << 4 // Invert image colors: no
|
||||
| 0b0111 << 0 // Interval between VCOM on and image data (default)
|
||||
);
|
||||
|
||||
// Load the various LUTs
|
||||
sendCommand(0x20); // VCOM
|
||||
sendData(LUT_FAST_VCOMDC, sizeof(LUT_FAST_VCOMDC));
|
||||
|
||||
sendCommand(0x21); // White -> White
|
||||
sendData(LUT_FAST_WW, sizeof(LUT_FAST_WW));
|
||||
|
||||
sendCommand(0x22); // Black -> White
|
||||
sendData(LUT_FAST_BW, sizeof(LUT_FAST_BW));
|
||||
|
||||
sendCommand(0x23); // White -> Black
|
||||
sendData(LUT_FAST_WB, sizeof(LUT_FAST_WB));
|
||||
|
||||
sendCommand(0x24); // Black -> Black
|
||||
sendData(LUT_FAST_BB, sizeof(LUT_FAST_BB));
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::writeNewImage()
|
||||
{
|
||||
sendCommand(0x13);
|
||||
sendData(buffer, bufferSize);
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::writeOldImage()
|
||||
{
|
||||
sendCommand(0x10);
|
||||
sendData(buffer, bufferSize);
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::detachFromUpdate()
|
||||
{
|
||||
// To save power / cycles, displays can choose to specify an "expected duration" for various refresh types
|
||||
// If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed
|
||||
// If not implemented, we'll just poll right from the get-go
|
||||
switch (updateType) {
|
||||
case FULL:
|
||||
EInk::beginPolling(10, 3650);
|
||||
break;
|
||||
case FAST:
|
||||
EInk::beginPolling(10, 720);
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
bool LCMEN213EFC1::isUpdateDone()
|
||||
{
|
||||
// Busy when LOW
|
||||
if (digitalRead(pin_busy) == LOW)
|
||||
return false;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::finalizeUpdate()
|
||||
{
|
||||
// Power off the panel voltages
|
||||
sendCommand(0x02);
|
||||
wait();
|
||||
|
||||
// Put a copy of the image into the "old memory".
|
||||
// Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place
|
||||
// We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc.
|
||||
if (updateType != FULL) {
|
||||
writeOldImage();
|
||||
wait();
|
||||
}
|
||||
}
|
||||
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
|
||||
E-Ink display driver
|
||||
- LCMEN213EFC1
|
||||
- Manufacturer: Wisevast
|
||||
- Size: 2.13 inch
|
||||
- Resolution: 122px x 250px
|
||||
- Flex connector marking: HINK-E0213A162-FPC-A0 (Hidden, printed on back-side)
|
||||
|
||||
Note: this display uses an uncommon controller IC, Fitipower JD79656.
|
||||
It is implemented as a "one-off", directly inheriting the EInk base class, unlike SSD16XX displays.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./EInk.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
|
||||
class LCMEN213EFC1 : public EInk
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 122;
|
||||
static constexpr uint32_t height = 250;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
|
||||
public:
|
||||
LCMEN213EFC1();
|
||||
void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst);
|
||||
void update(uint8_t *imageData, UpdateTypes type) override;
|
||||
|
||||
protected:
|
||||
void wait();
|
||||
void reset();
|
||||
void sendCommand(const uint8_t command);
|
||||
void sendData(const uint8_t data);
|
||||
void sendData(const uint8_t *data, uint32_t size);
|
||||
void configFull(); // Configure display for FULL refresh
|
||||
void configFast(); // Configure display for FAST refresh
|
||||
void writeNewImage();
|
||||
void writeOldImage();
|
||||
|
||||
void detachFromUpdate();
|
||||
bool isUpdateDone();
|
||||
void finalizeUpdate();
|
||||
|
||||
protected:
|
||||
uint8_t bufferOffsetX; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring?
|
||||
uint8_t bufferRowSize; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes)
|
||||
uint32_t bufferSize; // In bytes. Rows * Columns
|
||||
uint8_t *buffer;
|
||||
UpdateTypes updateType;
|
||||
|
||||
uint8_t pin_dc, pin_cs, pin_busy, pin_rst;
|
||||
SPIClass *spi;
|
||||
SPISettings spiSettings = SPISettings(6000000, MSBFIRST, SPI_MODE0);
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
|
@ -0,0 +1,82 @@
|
|||
# NicheGraphics - E-Ink Driver
|
||||
|
||||
A driver for E-Ink SPI displays. Suitable for re-use by various NicheGraphics UIs.
|
||||
|
||||
Your UI should use the class `NicheGraphics::Drivers::EInk` .
|
||||
When you set up a hardware variant, you will use one of specific display model classes, which extend the EInk class.
|
||||
|
||||
An example setup might look like this:
|
||||
|
||||
```cpp
|
||||
void setupNicheGraphics()
|
||||
{
|
||||
using namespace NicheGraphics;
|
||||
|
||||
// An imaginary UI
|
||||
YourCustomUI *yourUI = new YourCustomUI();
|
||||
|
||||
// Setup SPI
|
||||
SPIClass *hspi = new SPIClass(HSPI);
|
||||
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS);
|
||||
|
||||
// Setup Enk driver
|
||||
Drivers::EInk *driver = new Drivers::DEPG0290BNS800;
|
||||
driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY);
|
||||
|
||||
// Pass the driver to your UI
|
||||
YourUI::driver = driver;
|
||||
}
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
### `update(uint8_t *imageData, UpdateTypes type, bool async=true)`
|
||||
|
||||
Update the image on the display
|
||||
|
||||
- _`imageData`_ to draw to the display.
|
||||
- _`type`_ which type of update to perform.
|
||||
- `FULL`
|
||||
- `FAST`
|
||||
- (Other custom types may be possible)
|
||||
- _`async`_ whether to wait for update to complete, or continue code execution
|
||||
|
||||
The imageData is a 1-bit image. X-Pixels are 8-per byte, with the MSB being the leftmost pixel. This was not an InkHUD design decision; it is the raw format accepted by the E-Ink display controllers ICs.
|
||||
|
||||
_To-do: add a helper method to `InkHUD::Drivers::EInk` to do this arithmetic for you._
|
||||
|
||||
```cpp
|
||||
uint16_t w = driver::width();
|
||||
uint16_t h = driver::height();
|
||||
|
||||
uint8_t image[ (w/8) * h ]; // X pixels are 8-per-byte
|
||||
|
||||
image[0] |= (1 << 7); // Set pixel x=0, y=0
|
||||
image[0] |= (1 << 0); // Set pixel x=7, y=0
|
||||
image[1] |= (1 << 7); // Set pixel x=8, y=0
|
||||
|
||||
uint8_t x = 12;
|
||||
uint8_t y = 2;
|
||||
uint8_t yBytes = y * (w/8);
|
||||
uint8_t xBytes = x / 8;
|
||||
uint8_t xBits = (7-x) % 8;
|
||||
image[yByte + xByte] |= (1 << xBits); // Set pixel x=12, y=2
|
||||
```
|
||||
|
||||
### `supports(UpdateTypes type)`
|
||||
|
||||
Check if display supports a specific update type. `true` if supported.
|
||||
|
||||
- _`type`_ type to check
|
||||
|
||||
### `busy()`
|
||||
|
||||
Check if display is already performing an `update()`. `true` if already updating.
|
||||
|
||||
### `width()`
|
||||
|
||||
Width of the display, in pixels. Note: most displays are portait. Your UI will need to implement rotation in software.
|
||||
|
||||
### `height()`
|
||||
|
||||
Height of the display, in pixels. Note: most displays are portrait. Your UI will need to implement rotation in software.
|
|
@ -0,0 +1,227 @@
|
|||
#include "./SSD16XX.h"
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
SSD16XX::SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX)
|
||||
: EInk(width, height, supported), bufferOffsetX(bufferOffsetX)
|
||||
{
|
||||
// Pre-calculate size of the image buffer, for convenience
|
||||
|
||||
// Determine the X dimension 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.
|
||||
bufferRowSize = ((width - 1) / 8) + 1;
|
||||
|
||||
// Total size of image buffer, in bytes.
|
||||
bufferSize = bufferRowSize * height;
|
||||
}
|
||||
|
||||
void SSD16XX::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
|
||||
{
|
||||
this->spi = spi;
|
||||
this->pin_dc = pin_dc;
|
||||
this->pin_cs = pin_cs;
|
||||
this->pin_busy = pin_busy;
|
||||
this->pin_rst = pin_rst;
|
||||
|
||||
pinMode(pin_dc, OUTPUT);
|
||||
pinMode(pin_cs, OUTPUT);
|
||||
pinMode(pin_busy, INPUT);
|
||||
|
||||
// If using a reset pin, hold high
|
||||
// Reset is active low for solmon systech ICs
|
||||
if (pin_rst != 0xFF)
|
||||
pinMode(pin_rst, INPUT_PULLUP);
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
void SSD16XX::wait()
|
||||
{
|
||||
// Busy when HIGH
|
||||
while (digitalRead(pin_busy) == HIGH)
|
||||
yield();
|
||||
}
|
||||
|
||||
void SSD16XX::reset()
|
||||
{
|
||||
// Check if reset pin is defined
|
||||
if (pin_rst != 0xFF) {
|
||||
pinMode(pin_rst, OUTPUT);
|
||||
digitalWrite(pin_rst, LOW);
|
||||
delay(50);
|
||||
pinMode(pin_rst, INPUT_PULLUP);
|
||||
wait();
|
||||
}
|
||||
|
||||
sendCommand(0x12);
|
||||
wait();
|
||||
}
|
||||
|
||||
void SSD16XX::sendCommand(const uint8_t command)
|
||||
{
|
||||
spi->beginTransaction(spiSettings);
|
||||
digitalWrite(pin_dc, LOW); // DC pin low indicates command
|
||||
digitalWrite(pin_cs, LOW);
|
||||
spi->transfer(command);
|
||||
digitalWrite(pin_cs, HIGH);
|
||||
digitalWrite(pin_dc, HIGH);
|
||||
spi->endTransaction();
|
||||
}
|
||||
|
||||
void SSD16XX::sendData(uint8_t data)
|
||||
{
|
||||
// spi->beginTransaction(spiSettings);
|
||||
// digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command
|
||||
// digitalWrite(pin_cs, LOW);
|
||||
// spi->transfer(data);
|
||||
// digitalWrite(pin_cs, HIGH);
|
||||
// digitalWrite(pin_dc, HIGH);
|
||||
// spi->endTransaction();
|
||||
sendData(&data, 1);
|
||||
}
|
||||
|
||||
void SSD16XX::sendData(const uint8_t *data, uint32_t size)
|
||||
{
|
||||
spi->beginTransaction(spiSettings);
|
||||
digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command
|
||||
digitalWrite(pin_cs, LOW);
|
||||
|
||||
// Platform-specific SPI command
|
||||
#if defined(ARCH_ESP32)
|
||||
spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer
|
||||
#elif defined(ARCH_NRF52)
|
||||
spi->transfer(data, NULL, size); // NULL for a "write only" transfer
|
||||
#else
|
||||
#error Not implemented yet? Feel free to add other platforms here.
|
||||
#endif
|
||||
|
||||
digitalWrite(pin_cs, HIGH);
|
||||
digitalWrite(pin_dc, HIGH);
|
||||
spi->endTransaction();
|
||||
}
|
||||
|
||||
void SSD16XX::configFullscreen()
|
||||
{
|
||||
// Placing this code in a separate method because it's probably pretty consistent between displays
|
||||
// Should make it tidier to override SSD16XX::configure
|
||||
|
||||
// Define the boundaries of the "fullscreen" region, for the controller IC
|
||||
static const uint16_t sx = bufferOffsetX; // Notice the offset
|
||||
static const uint16_t sy = 0;
|
||||
static const uint16_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this
|
||||
static const uint16_t ey = height;
|
||||
|
||||
// Split into bytes
|
||||
static const uint8_t sy1 = sy & 0xFF;
|
||||
static const uint8_t sy2 = (sy >> 8) & 0xFF;
|
||||
static const uint8_t ey1 = ey & 0xFF;
|
||||
static const uint8_t ey2 = (ey >> 8) & 0xFF;
|
||||
|
||||
// Data entry mode - Left to Right, Top to Bottom
|
||||
sendCommand(0x11);
|
||||
sendData(0x03);
|
||||
|
||||
// Select controller IC memory region to display a fullscreen image
|
||||
sendCommand(0x44); // Memory X start - end
|
||||
sendData(sx);
|
||||
sendData(ex);
|
||||
sendCommand(0x45); // Memory Y start - end
|
||||
sendData(sy1);
|
||||
sendData(sy2);
|
||||
sendData(ey1);
|
||||
sendData(ey2);
|
||||
|
||||
// Place the cursor at the start of this memory region, ready to send image data x=0 y=0
|
||||
sendCommand(0x4E); // Memory cursor X
|
||||
sendData(sx);
|
||||
sendCommand(0x4F); // Memory cursor y
|
||||
sendData(sy1);
|
||||
sendData(sy2);
|
||||
}
|
||||
|
||||
void SSD16XX::update(uint8_t *imageData, UpdateTypes type)
|
||||
{
|
||||
this->updateType = type;
|
||||
this->buffer = imageData;
|
||||
|
||||
reset();
|
||||
|
||||
configFullscreen();
|
||||
configScanning(); // Virtual, unused by base class
|
||||
configVoltages(); // Virtual, unused by base class
|
||||
configWaveform(); // Virtual, unused by base class
|
||||
wait();
|
||||
|
||||
if (updateType == FULL) {
|
||||
writeNewImage();
|
||||
writeOldImage();
|
||||
} else {
|
||||
writeNewImage();
|
||||
}
|
||||
|
||||
configUpdateSequence();
|
||||
sendCommand(0x20); // Begin executing the update
|
||||
|
||||
// Let the update run async, on display hardware. Base class will poll completion, then finalize.
|
||||
// For a blocking update, call await after update
|
||||
detachFromUpdate();
|
||||
}
|
||||
|
||||
// Send SPI commands for controller IC to begin executing the refresh operation
|
||||
void SSD16XX::configUpdateSequence()
|
||||
{
|
||||
switch (updateType) {
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Non-differential, load waveform from OTP
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SSD16XX::writeNewImage()
|
||||
{
|
||||
sendCommand(0x24);
|
||||
sendData(buffer, bufferSize);
|
||||
}
|
||||
|
||||
void SSD16XX::writeOldImage()
|
||||
{
|
||||
sendCommand(0x26);
|
||||
sendData(buffer, bufferSize);
|
||||
}
|
||||
|
||||
void SSD16XX::detachFromUpdate()
|
||||
{
|
||||
// To save power / cycles, displays can choose to specify an "expected duration" for various refresh types
|
||||
// If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed
|
||||
// If not implemented, we'll just poll right from the get-go
|
||||
switch (updateType) {
|
||||
default:
|
||||
EInk::beginPolling(100, 0);
|
||||
}
|
||||
}
|
||||
|
||||
bool SSD16XX::isUpdateDone()
|
||||
{
|
||||
// Busy when HIGH
|
||||
if (digitalRead(pin_busy) == HIGH)
|
||||
return false;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
void SSD16XX::finalizeUpdate()
|
||||
{
|
||||
// Put a copy of the image into the "old memory".
|
||||
// Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place
|
||||
// We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc.
|
||||
if (updateType != FULL) {
|
||||
writeNewImage(); // Only required by some controller variants. Todo: Override just for GDEY0154D678?
|
||||
writeOldImage();
|
||||
sendCommand(0x7F); // Terminate image write without update
|
||||
wait();
|
||||
}
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
|
||||
E-Ink base class for displays based on SSD16XX
|
||||
|
||||
Most (but not all) SPI E-Ink displays use this family of controller IC.
|
||||
Implementing new SSD16XX displays should be fairly painless.
|
||||
See DEPG0154BNS800 and DEPG0290BNS800 for examples.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./EInk.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
|
||||
class SSD16XX : public EInk
|
||||
{
|
||||
public:
|
||||
SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX = 0);
|
||||
virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1);
|
||||
virtual void update(uint8_t *imageData, UpdateTypes type) override;
|
||||
|
||||
protected:
|
||||
virtual void wait();
|
||||
virtual void reset();
|
||||
virtual void sendCommand(const uint8_t command);
|
||||
virtual void sendData(const uint8_t data);
|
||||
virtual void sendData(const uint8_t *data, uint32_t size);
|
||||
virtual void configFullscreen(); // Select memory region on controller IC
|
||||
virtual void configScanning() {} // Optional. First & last gates, scan direction, etc
|
||||
virtual void configVoltages() {} // Optional. Manual panel voltages, soft-start, etc
|
||||
virtual void configWaveform() {} // Optional. LUT, panel border, temperature sensor, etc
|
||||
virtual void configUpdateSequence(); // Tell controller IC which operations to run
|
||||
|
||||
virtual void writeNewImage();
|
||||
virtual void writeOldImage();
|
||||
|
||||
virtual void detachFromUpdate();
|
||||
virtual bool isUpdateDone() override;
|
||||
virtual void finalizeUpdate() override;
|
||||
|
||||
protected:
|
||||
uint8_t bufferOffsetX; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring?
|
||||
uint8_t bufferRowSize; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes)
|
||||
uint32_t bufferSize; // In bytes. Rows * Columns
|
||||
uint8_t *buffer;
|
||||
UpdateTypes updateType;
|
||||
|
||||
uint8_t pin_dc, pin_cs, pin_busy, pin_rst;
|
||||
SPIClass *spi;
|
||||
SPISettings spiSettings = SPISettings(4000000, MSBFIRST, SPI_MODE0);
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
|
@ -0,0 +1,3 @@
|
|||
# NicheGraphics - Drivers
|
||||
|
||||
Common drivers which can be used by various NicheGrapihcs UIs
|
|
@ -0,0 +1,140 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
/*
|
||||
|
||||
Re-usable NicheGraphics tool
|
||||
|
||||
Save settings / data to flash, without use of the Meshtastic Protobufs
|
||||
Avoid bloating everyone's protobuf code for our one-off UI implementations
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "SafeFile.h"
|
||||
|
||||
namespace NicheGraphics
|
||||
{
|
||||
|
||||
template <typename T> class FlashData
|
||||
{
|
||||
private:
|
||||
static std::string getFilename(const char *label)
|
||||
{
|
||||
std::string filename;
|
||||
filename += "/NicheGraphics";
|
||||
filename += "/";
|
||||
filename += label;
|
||||
filename += ".data";
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
static uint32_t getHash(T *data)
|
||||
{
|
||||
uint32_t hash = 0;
|
||||
|
||||
// Sum all bytes of the image buffer together
|
||||
for (uint32_t i = 0; i < sizeof(T); i++)
|
||||
hash ^= ((uint8_t *)data)[i] + 1;
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
public:
|
||||
static bool load(T *data, const char *label)
|
||||
{
|
||||
// Set false if we run into issues
|
||||
bool okay = true;
|
||||
|
||||
// Get a filename based on the label
|
||||
std::string filename = getFilename(label);
|
||||
|
||||
#ifdef FSCom
|
||||
|
||||
// Check that the file *does* actually exist
|
||||
if (!FSCom.exists(filename.c_str())) {
|
||||
LOG_WARN("'%s' not found. Using default values", filename.c_str());
|
||||
okay = false;
|
||||
return okay;
|
||||
}
|
||||
|
||||
// Open the file
|
||||
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
|
||||
|
||||
// If opened, start reading
|
||||
if (f) {
|
||||
LOG_INFO("Loading NicheGraphics data '%s'", filename.c_str());
|
||||
|
||||
// Create an object which will received data from flash
|
||||
// We read here first, so we can verify the checksum, without committing to overwriting the *data object
|
||||
// Allows us to retain any defaults that might be set after we declared *data, but before loading settings,
|
||||
// in case the flash values are corrupt
|
||||
T flashData;
|
||||
|
||||
// Read the actual data
|
||||
f.readBytes((char *)&flashData, sizeof(T));
|
||||
|
||||
// Read the hash
|
||||
uint32_t savedHash = 0;
|
||||
f.readBytes((char *)&savedHash, sizeof(savedHash));
|
||||
|
||||
// Calculate hash of the loaded data, then compare with the saved hash
|
||||
// If hash looks good, copy the values to the main data object
|
||||
uint32_t calculatedHash = getHash(&flashData);
|
||||
if (savedHash != calculatedHash) {
|
||||
LOG_WARN("'%s' is corrupt (hash mismatch). Using default values", filename.c_str());
|
||||
okay = false;
|
||||
} else
|
||||
*data = flashData;
|
||||
|
||||
f.close();
|
||||
} else {
|
||||
LOG_ERROR("Could not open / read %s", filename.c_str());
|
||||
okay = false;
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("Filesystem not implemented");
|
||||
state = LoadFileState::NO_FILESYSTEM;
|
||||
okay = false;
|
||||
#endif
|
||||
return okay;
|
||||
}
|
||||
|
||||
// Save module's custom data (settings?) to flash. Does use protobufs
|
||||
static void save(T *data, const char *label)
|
||||
{
|
||||
// Get a filename based on the label
|
||||
std::string filename = getFilename(label);
|
||||
|
||||
#ifdef FSCom
|
||||
FSCom.mkdir("/NicheGraphics");
|
||||
|
||||
auto f = SafeFile(filename.c_str(), true); // "true": full atomic. Write new data to temp file, then rename.
|
||||
|
||||
LOG_INFO("Saving %s", filename.c_str());
|
||||
|
||||
// Calculate a hash of the data
|
||||
uint32_t hash = getHash(data);
|
||||
|
||||
f.write((uint8_t *)data, sizeof(T)); // Write the actualy data
|
||||
f.write((uint8_t *)&hash, sizeof(hash)); // Append the hash
|
||||
|
||||
// f.flush();
|
||||
|
||||
bool writeSucceeded = f.close();
|
||||
|
||||
if (!writeSucceeded) {
|
||||
LOG_ERROR("Can't write data!");
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("ERROR: Filesystem not implemented\n");
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics
|
||||
|
||||
#endif
|
|
@ -0,0 +1,129 @@
|
|||
#pragma once
|
||||
|
||||
const uint8_t FreeSans6pt7bBitmaps[] PROGMEM = {
|
||||
0xAA, 0xA8, 0xC0, 0xF6, 0xA0, 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00,
|
||||
0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, 0x70, 0x91, 0x23, 0x86, 0x12, 0xA2, 0x4E, 0xF4, 0xE0,
|
||||
0x5A, 0xAA, 0x94, 0x89, 0x12, 0x49, 0x29, 0x00, 0x27, 0x50, 0x21, 0x3E, 0x42, 0x00, 0xE0, 0xC0, 0x80, 0x24, 0xA4, 0xA4, 0x80,
|
||||
0x74, 0xE3, 0x18, 0xC6, 0x33, 0x70, 0x27, 0x92, 0x49, 0x20, 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, 0x79, 0x30, 0x43, 0x18,
|
||||
0x10, 0x71, 0x78, 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, 0x7D, 0x04, 0x1E, 0x44, 0x10, 0x51, 0x78, 0x74, 0x61, 0xE8, 0xC6,
|
||||
0x31, 0x70, 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53,
|
||||
0x78, 0x82, 0x87, 0x01, 0xF1, 0x83, 0x04, 0xF8, 0x3E, 0x07, 0x06, 0x36, 0x40, 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, 0x0F, 0x86,
|
||||
0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42,
|
||||
0x42, 0xC3, 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, 0xF9, 0x0A, 0x1C,
|
||||
0x18, 0x30, 0x61, 0xC2, 0xF8, 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, 0x1E, 0x61,
|
||||
0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, 0xFF, 0x80, 0x08, 0x42, 0x10, 0x87,
|
||||
0x29, 0x70, 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, 0xC3, 0xC3, 0xC3, 0xA5, 0xA5,
|
||||
0xA5, 0x99, 0x99, 0x99, 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6,
|
||||
0x1E, 0x00, 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1F, 0x00, 0x00,
|
||||
0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04,
|
||||
0x08, 0x10, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, 0xC2, 0x42, 0x42, 0x64, 0x24, 0x24, 0x38, 0x18, 0x18, 0xC4, 0x28,
|
||||
0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, 0x42, 0x66, 0x24, 0x18, 0x18, 0x18, 0x24, 0x46, 0x42, 0xC3,
|
||||
0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, 0xEA, 0xAA, 0xAB, 0x92, 0x24,
|
||||
0x89, 0x20, 0xE9, 0x24, 0x92, 0x49, 0x70, 0x46, 0xA9, 0x10, 0xFE, 0x40, 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, 0x84, 0x3D, 0x18,
|
||||
0xC6, 0x31, 0xF0, 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, 0x39, 0x38, 0x7F, 0x81, 0x13,
|
||||
0x80, 0x6B, 0xA4, 0x92, 0x40, 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, 0xBF, 0x80,
|
||||
0x45, 0x55, 0x57, 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, 0xFF, 0x80, 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, 0xF4, 0x63, 0x18,
|
||||
0xC6, 0x20, 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04,
|
||||
0xF2, 0x49, 0x20, 0x79, 0x24, 0x1C, 0x0B, 0x27, 0x80, 0x5D, 0x24, 0x93, 0x8C, 0x63, 0x18, 0xCF, 0xA0, 0x85, 0x24, 0x92, 0x30,
|
||||
0xC3, 0x00, 0x89, 0x2C, 0x96, 0x4A, 0xA5, 0x61, 0x30, 0x98, 0x49, 0x23, 0x08, 0x31, 0x2C, 0x80, 0x89, 0x24, 0x94, 0x50, 0xC2,
|
||||
0x08, 0x21, 0x00, 0x78, 0x44, 0x46, 0x23, 0xE0, 0x6A, 0xAA, 0xA9, 0xFF, 0xE0, 0x95, 0x55, 0x56, 0x66, 0x60};
|
||||
|
||||
const GFXglyph FreeSans6pt7bGlyphs[] PROGMEM = {{0, 0, 0, 3, 0, 1}, // 0x20 ' '
|
||||
{0, 2, 9, 4, 1, -8}, // 0x21 '!'
|
||||
{3, 4, 3, 4, 0, -8}, // 0x22 '"'
|
||||
{5, 7, 8, 7, 0, -7}, // 0x23 '#'
|
||||
{12, 6, 11, 7, 0, -9}, // 0x24 '$'
|
||||
{21, 10, 9, 11, 0, -8}, // 0x25 '%'
|
||||
{33, 7, 9, 8, 1, -8}, // 0x26 '&'
|
||||
{41, 1, 3, 2, 1, -8}, // 0x27 '''
|
||||
{42, 2, 11, 4, 1, -8}, // 0x28 '('
|
||||
{45, 3, 11, 4, 0, -8}, // 0x29 ')'
|
||||
{50, 4, 3, 5, 0, -8}, // 0x2A '*'
|
||||
{52, 5, 5, 7, 1, -4}, // 0x2B '+'
|
||||
{56, 1, 3, 3, 1, 0}, // 0x2C ','
|
||||
{57, 2, 1, 4, 1, -3}, // 0x2D '-'
|
||||
{58, 1, 1, 3, 1, 0}, // 0x2E '.'
|
||||
{59, 3, 9, 3, 0, -8}, // 0x2F '/'
|
||||
{63, 5, 9, 7, 1, -8}, // 0x30 '0'
|
||||
{69, 3, 9, 7, 1, -8}, // 0x31 '1'
|
||||
{73, 6, 9, 7, 0, -8}, // 0x32 '2'
|
||||
{80, 6, 9, 7, 0, -8}, // 0x33 '3'
|
||||
{87, 6, 9, 7, 0, -8}, // 0x34 '4'
|
||||
{94, 6, 9, 7, 0, -8}, // 0x35 '5'
|
||||
{101, 5, 9, 7, 1, -8}, // 0x36 '6'
|
||||
{107, 5, 9, 7, 1, -8}, // 0x37 '7'
|
||||
{113, 6, 9, 7, 0, -8}, // 0x38 '8'
|
||||
{120, 6, 9, 7, 0, -8}, // 0x39 '9'
|
||||
{127, 1, 7, 3, 1, -6}, // 0x3A ':'
|
||||
{128, 1, 8, 3, 1, -5}, // 0x3B ';'
|
||||
{129, 5, 6, 7, 1, -5}, // 0x3C '<'
|
||||
{133, 5, 3, 7, 1, -3}, // 0x3D '='
|
||||
{135, 5, 6, 7, 1, -5}, // 0x3E '>'
|
||||
{139, 5, 9, 7, 1, -8}, // 0x3F '?'
|
||||
{145, 11, 11, 12, 0, -8}, // 0x40 '@'
|
||||
{161, 8, 9, 8, 0, -8}, // 0x41 'A'
|
||||
{170, 6, 9, 8, 1, -8}, // 0x42 'B'
|
||||
{177, 8, 9, 9, 0, -8}, // 0x43 'C'
|
||||
{186, 7, 9, 8, 1, -8}, // 0x44 'D'
|
||||
{194, 6, 9, 8, 1, -8}, // 0x45 'E'
|
||||
{201, 6, 9, 7, 1, -8}, // 0x46 'F'
|
||||
{208, 8, 9, 9, 0, -8}, // 0x47 'G'
|
||||
{217, 7, 9, 9, 1, -8}, // 0x48 'H'
|
||||
{225, 1, 9, 3, 1, -8}, // 0x49 'I'
|
||||
{227, 5, 9, 6, 0, -8}, // 0x4A 'J'
|
||||
{233, 7, 9, 8, 1, -8}, // 0x4B 'K'
|
||||
{241, 5, 9, 7, 1, -8}, // 0x4C 'L'
|
||||
{247, 8, 9, 10, 1, -8}, // 0x4D 'M'
|
||||
{256, 7, 9, 9, 1, -8}, // 0x4E 'N'
|
||||
{264, 9, 9, 9, 0, -8}, // 0x4F 'O'
|
||||
{275, 6, 9, 8, 1, -8}, // 0x50 'P'
|
||||
{282, 9, 10, 9, 0, -8}, // 0x51 'Q'
|
||||
{294, 7, 9, 9, 1, -8}, // 0x52 'R'
|
||||
{302, 6, 9, 8, 1, -8}, // 0x53 'S'
|
||||
{309, 7, 9, 8, 0, -8}, // 0x54 'T'
|
||||
{317, 7, 9, 9, 1, -8}, // 0x55 'U'
|
||||
{325, 8, 9, 8, 0, -8}, // 0x56 'V'
|
||||
{334, 11, 9, 11, 0, -8}, // 0x57 'W'
|
||||
{347, 8, 9, 8, 0, -8}, // 0x58 'X'
|
||||
{356, 8, 9, 8, 0, -8}, // 0x59 'Y'
|
||||
{365, 7, 9, 7, 0, -8}, // 0x5A 'Z'
|
||||
{373, 2, 12, 3, 1, -8}, // 0x5B '['
|
||||
{376, 3, 9, 3, 0, -8}, // 0x5C '\'
|
||||
{380, 3, 12, 3, 0, -8}, // 0x5D ']'
|
||||
{385, 4, 5, 6, 1, -8}, // 0x5E '^'
|
||||
{388, 7, 1, 7, 0, 2}, // 0x5F '_'
|
||||
{389, 3, 1, 3, 0, -8}, // 0x60 '`'
|
||||
{390, 6, 7, 7, 0, -6}, // 0x61 'a'
|
||||
{396, 5, 9, 7, 1, -8}, // 0x62 'b'
|
||||
{402, 6, 7, 6, 0, -6}, // 0x63 'c'
|
||||
{408, 6, 9, 7, 0, -8}, // 0x64 'd'
|
||||
{415, 6, 7, 6, 0, -6}, // 0x65 'e'
|
||||
{421, 3, 9, 3, 0, -8}, // 0x66 'f'
|
||||
{425, 6, 10, 7, 0, -6}, // 0x67 'g'
|
||||
{433, 5, 9, 6, 1, -8}, // 0x68 'h'
|
||||
{439, 1, 9, 3, 1, -8}, // 0x69 'i'
|
||||
{441, 2, 12, 3, 0, -8}, // 0x6A 'j'
|
||||
{444, 5, 9, 6, 1, -8}, // 0x6B 'k'
|
||||
{450, 1, 9, 3, 1, -8}, // 0x6C 'l'
|
||||
{452, 8, 7, 10, 1, -6}, // 0x6D 'm'
|
||||
{459, 5, 7, 6, 1, -6}, // 0x6E 'n'
|
||||
{464, 6, 7, 6, 0, -6}, // 0x6F 'o'
|
||||
{470, 5, 9, 7, 1, -6}, // 0x70 'p'
|
||||
{476, 6, 9, 7, 0, -6}, // 0x71 'q'
|
||||
{483, 3, 7, 4, 1, -6}, // 0x72 'r'
|
||||
{486, 6, 7, 6, 0, -6}, // 0x73 's'
|
||||
{492, 3, 8, 3, 0, -7}, // 0x74 't'
|
||||
{495, 5, 7, 6, 1, -6}, // 0x75 'u'
|
||||
{500, 6, 7, 6, 0, -6}, // 0x76 'v'
|
||||
{506, 9, 7, 9, 0, -6}, // 0x77 'w'
|
||||
{514, 6, 7, 6, 0, -6}, // 0x78 'x'
|
||||
{520, 6, 10, 6, 0, -6}, // 0x79 'y'
|
||||
{528, 5, 7, 6, 0, -6}, // 0x7A 'z'
|
||||
{533, 2, 12, 4, 1, -8}, // 0x7B '{'
|
||||
{536, 1, 11, 3, 1, -8}, // 0x7C '|'
|
||||
{538, 2, 12, 4, 1, -8}, // 0x7D '}'
|
||||
{541, 6, 2, 6, 0, -4}}; // 0x7E '~'
|
||||
|
||||
const GFXfont FreeSans6pt7b PROGMEM = {(uint8_t *)FreeSans6pt7bBitmaps, (GFXglyph *)FreeSans6pt7bGlyphs, 0x20, 0x7E, 14};
|
||||
|
||||
// Approx. 1215 bytes
|
|
@ -0,0 +1,302 @@
|
|||
/*
|
||||
|
||||
Uses Windows-1251 encoding to map translingual Cyrillic characters to range between (uint8_t)127 and (uint8_t)255
|
||||
https://en.wikipedia.org/wiki/Windows-1251
|
||||
|
||||
Cyrillic characters present to the firmware as UTF8.
|
||||
A Niche Graphics implementation needs to identify these, and subsitute the appropriate Windows-1251 char value.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
const uint8_t FreeSans6pt8bCyrillicBitmaps[] PROGMEM = {
|
||||
0xFF, 0xA0, 0xC0, 0xFF, 0xA0, 0xC0, 0xB6, 0x80, 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, 0x31, 0x75, 0x54, 0x78, 0x79, 0x75,
|
||||
0x7C, 0x41, 0x00, 0x01, 0x1C, 0x49, 0x22, 0x50, 0x74, 0x02, 0x60, 0xA4, 0x49, 0x11, 0xC0, 0x21, 0x44, 0x94, 0x62, 0x59, 0xE2,
|
||||
0xF4, 0xE0, 0x6A, 0xAA, 0x90, 0x48, 0x92, 0x49, 0x4A, 0x00, 0x5D, 0x40, 0x21, 0x09, 0xF2, 0x10, 0xE0, 0xC0, 0x80, 0x25, 0x25,
|
||||
0x24, 0x26, 0xA3, 0x18, 0xC6, 0x31, 0xF0, 0x27, 0x92, 0x49, 0x20, 0x11, 0xB4, 0x41, 0x0C, 0xC6, 0x10, 0xFC, 0x26, 0xA2, 0x13,
|
||||
0x04, 0x31, 0xF0, 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, 0xFF, 0xE1, 0x4D, 0x84, 0x31, 0xF0, 0x26, 0xE3, 0x0F, 0x46, 0x31,
|
||||
0xF0, 0xFF, 0xC4, 0x22, 0x11, 0x08, 0x40, 0x11, 0xA4, 0x51, 0x39, 0x1C, 0x51, 0x78, 0x11, 0xA4, 0x71, 0x45, 0xF0, 0x51, 0x78,
|
||||
0xC0, 0x30, 0xC0, 0x36, 0x1F, 0x20, 0xE0, 0x80, 0xF8, 0x3E, 0xC1, 0xC2, 0xE8, 0x00, 0x74, 0x62, 0x11, 0x10, 0x80, 0x20, 0x0F,
|
||||
0x06, 0x18, 0x81, 0xA7, 0xD4, 0x93, 0x22, 0x64, 0x4A, 0x7E, 0x60, 0x06, 0x00, 0x3C, 0x00, 0x18, 0x18, 0x1C, 0x24, 0x24, 0x7E,
|
||||
0x42, 0x42, 0xC3, 0xFA, 0x38, 0x61, 0xFA, 0x18, 0x61, 0xFC, 0x38, 0x8A, 0x0C, 0x08, 0x10, 0x20, 0xE3, 0x7C, 0xF9, 0x1A, 0x1C,
|
||||
0x18, 0x30, 0x60, 0xC2, 0xF8, 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, 0x3C, 0x46,
|
||||
0x82, 0x80, 0x8F, 0x81, 0x83, 0xC3, 0x7D, 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, 0xFF, 0x80, 0x08, 0x42, 0x10, 0x86,
|
||||
0x31, 0x78, 0x87, 0x1A, 0x65, 0x8F, 0x1A, 0x22, 0x42, 0x86, 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, 0xC3, 0xC3, 0xC3, 0xA5, 0xA5,
|
||||
0xA5, 0x99, 0x99, 0x99, 0x83, 0x87, 0x8D, 0x19, 0x32, 0x62, 0xC3, 0x86, 0x1E, 0x11, 0x90, 0x48, 0x1C, 0x0A, 0x05, 0x06, 0xC2,
|
||||
0x3E, 0x00, 0xFA, 0x18, 0x61, 0xFE, 0x08, 0x20, 0x80, 0x1E, 0x11, 0x90, 0x48, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x3F, 0x00, 0xFD,
|
||||
0x0E, 0x0C, 0x1F, 0xD0, 0xA0, 0xC1, 0x82, 0x7A, 0x18, 0x70, 0x78, 0x38, 0x61, 0x7C, 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08,
|
||||
0x10, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC3, 0x7C, 0xC3, 0x42, 0x42, 0x26, 0x24, 0x24, 0x14, 0x18, 0x18, 0xC4, 0x28, 0xC5,
|
||||
0x39, 0xA5, 0x24, 0xA4, 0x52, 0x8C, 0x71, 0x8C, 0x30, 0x80, 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xB3, 0x84, 0xC3, 0x42, 0x26, 0x24,
|
||||
0x18, 0x18, 0x08, 0x08, 0x08, 0x7E, 0x0C, 0x10, 0x41, 0x06, 0x08, 0x20, 0xFE, 0xEA, 0xAA, 0xAB, 0x92, 0x24, 0x89, 0x20, 0xED,
|
||||
0xB6, 0xDB, 0x6D, 0xF0, 0x46, 0xAA, 0x90, 0xFC, 0x90, 0xFC, 0x4F, 0x98, 0xFC, 0x84, 0x21, 0xF8, 0xC6, 0x31, 0xF0, 0x79, 0x18,
|
||||
0x20, 0x45, 0xE0, 0x04, 0x10, 0x5F, 0xC6, 0x18, 0x51, 0x7C, 0xFC, 0x7F, 0x08, 0xF8, 0x29, 0x74, 0x92, 0x40, 0x7D, 0x18, 0x61,
|
||||
0x45, 0xF0, 0x52, 0x30, 0x84, 0x21, 0xF8, 0xC6, 0x31, 0x88, 0xDF, 0x80, 0x51, 0x55, 0x56, 0x84, 0x21, 0x2A, 0x72, 0x92, 0x98,
|
||||
0xFF, 0x80, 0xFF, 0x99, 0x99, 0x99, 0x99, 0x99, 0xFC, 0x63, 0x18, 0xC4, 0x79, 0x18, 0x71, 0x45, 0xE0, 0xFC, 0x63, 0x18, 0xFA,
|
||||
0x10, 0x80, 0x7D, 0x18, 0x61, 0x45, 0xF0, 0x41, 0x04, 0xF2, 0x49, 0x00, 0x79, 0x07, 0x02, 0xCD, 0xE0, 0x4B, 0xA4, 0x93, 0x8C,
|
||||
0x63, 0x18, 0xFC, 0xCD, 0x24, 0x94, 0x30, 0xC0, 0x99, 0x59, 0x55, 0x56, 0x66, 0x26, 0x96, 0x66, 0x99, 0xCA, 0x52, 0x63, 0x18,
|
||||
0x84, 0x40, 0x78, 0xC4, 0x44, 0x7C, 0x6A, 0xAA, 0xA9, 0xFF, 0xF0, 0xC9, 0x24, 0x4A, 0x49, 0x40, 0xE8, 0xC0, 0xFE, 0x18, 0x61,
|
||||
0x86, 0x18, 0x61, 0xFC, 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, 0xC0, 0xC0, 0x10, 0x8F, 0xE0, 0x82,
|
||||
0x08, 0x20, 0x82, 0x08, 0x00, 0x64, 0x0F, 0x88, 0x88, 0x80, 0x3D, 0x0C, 0x2E, 0xF9, 0x04, 0x0F, 0x7C, 0x08, 0x81, 0x10, 0x22,
|
||||
0x04, 0x7C, 0x88, 0x51, 0x0A, 0x21, 0x87, 0xC0, 0x84, 0x10, 0x82, 0x10, 0x42, 0x0F, 0xFD, 0x08, 0xA1, 0x0C, 0x23, 0x87, 0xC0,
|
||||
0x10, 0x88, 0xE6, 0xB3, 0x8C, 0x28, 0x92, 0x28, 0xC0, 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, 0x83,
|
||||
0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xFE, 0x20, 0x40, 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51, 0x44, 0x11, 0x80, 0x78, 0x24, 0x13,
|
||||
0xC9, 0x14, 0x8E, 0x7C, 0x88, 0x44, 0x3F, 0xD1, 0x38, 0x8C, 0x78, 0x60, 0x9A, 0xCC, 0xA9, 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51,
|
||||
0x44, 0x8C, 0x63, 0x18, 0xFC, 0x80, 0x24, 0x33, 0x0A, 0x36, 0x45, 0x8E, 0x0C, 0x10, 0x60, 0x80, 0x70, 0x22, 0x95, 0xA8, 0xC4,
|
||||
0x23, 0x10, 0x08, 0x42, 0x10, 0x86, 0x31, 0x78, 0x07, 0xF8, 0x20, 0x82, 0x08, 0x20, 0x82, 0x00, 0x28, 0x0F, 0xE0, 0x82, 0x0F,
|
||||
0xE0, 0x82, 0x0F, 0xC0, 0x38, 0x8A, 0x0C, 0x0F, 0x90, 0x20, 0xE3, 0x7C, 0x51, 0x55, 0x56, 0xA1, 0x24, 0x92, 0x49, 0x00, 0xFF,
|
||||
0x80, 0xDF, 0x80, 0x27, 0xC9, 0x24, 0x8A, 0x28, 0xA2, 0x8B, 0xF8, 0x20, 0x80, 0x28, 0xA0, 0x1E, 0x47, 0xFC, 0x11, 0x78, 0x88,
|
||||
0x44, 0x32, 0x59, 0xDA, 0xCD, 0x66, 0x6B, 0x32, 0x89, 0x80, 0x79, 0x1F, 0x30, 0x45, 0xE0, 0x7A, 0x18, 0x70, 0x78, 0x38, 0x61,
|
||||
0x7C, 0x79, 0x07, 0x02, 0xCD, 0xE0, 0xB4, 0x24, 0x92, 0x40, 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, 0xFE, 0x08,
|
||||
0x20, 0xFE, 0x18, 0x61, 0xFC, 0xFA, 0x38, 0x61, 0xFA, 0x18, 0x61, 0xFC, 0xFE, 0x08, 0x20, 0x82, 0x08, 0x20, 0x80, 0x1F, 0x08,
|
||||
0x84, 0x42, 0x21, 0x10, 0x88, 0x44, 0x42, 0xFF, 0xC0, 0x60, 0x20, 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, 0x88, 0xA4, 0x9A,
|
||||
0x87, 0xC1, 0xC1, 0xF1, 0xAD, 0x92, 0x88, 0x80, 0x7A, 0x18, 0x41, 0x38, 0x18, 0x61, 0x7C, 0x87, 0x0E, 0x2C, 0x59, 0x34, 0x68,
|
||||
0xE1, 0xC2, 0x28, 0x22, 0x1C, 0x38, 0xB1, 0x64, 0xD1, 0xA3, 0x87, 0x08, 0x8E, 0x6B, 0x38, 0xC2, 0x89, 0x22, 0x8C, 0x3E, 0x44,
|
||||
0x89, 0x12, 0x24, 0x58, 0xA1, 0xC2, 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60,
|
||||
0xC1, 0x82, 0x3C, 0x46, 0x83, 0x81, 0x81, 0x81, 0x81, 0xC2, 0x7C, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x82, 0xFA, 0x18,
|
||||
0x61, 0xFE, 0x08, 0x20, 0x80, 0x38, 0x8A, 0x0C, 0x08, 0x10, 0x20, 0xE3, 0x7C, 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10,
|
||||
0xC2, 0x8D, 0x91, 0x63, 0x83, 0x04, 0x18, 0x20, 0x08, 0x1E, 0x32, 0xD1, 0x38, 0x8C, 0x4F, 0x2C, 0xFC, 0x08, 0x00, 0x87, 0x34,
|
||||
0x8C, 0x30, 0xC4, 0xB3, 0x84, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0xFF, 0x01, 0x01, 0x8E, 0x38, 0xE3, 0x8D, 0xF0,
|
||||
0xC3, 0x0C, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0xFF, 0x99, 0x4C, 0xA6, 0x53, 0x29, 0x94, 0xCA, 0x65, 0x32, 0xFF,
|
||||
0x80, 0x40, 0x20, 0xF0, 0x04, 0x01, 0x00, 0x40, 0x1F, 0x84, 0x21, 0x0C, 0x42, 0x1F, 0x00, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xDC,
|
||||
0x2E, 0x17, 0x0B, 0xF9, 0x80, 0x82, 0x08, 0x20, 0xFE, 0x18, 0x61, 0xF8, 0x79, 0x8A, 0x18, 0x13, 0xE0, 0x60, 0xC2, 0x7C, 0x87,
|
||||
0x26, 0x39, 0x06, 0x41, 0xF0, 0x64, 0x19, 0x06, 0x63, 0x8F, 0x80, 0x7E, 0x18, 0x61, 0x7C, 0xD6, 0x71, 0x84, 0x79, 0x11, 0xD9,
|
||||
0xCD, 0xD0, 0x0D, 0xC4, 0x1E, 0x47, 0x1C, 0x51, 0x78, 0xF4, 0xBD, 0x29, 0xF8, 0xF8, 0x88, 0x88, 0x3C, 0x48, 0x91, 0x22, 0x5F,
|
||||
0xE0, 0x80, 0x79, 0x1F, 0xF0, 0x45, 0xE0, 0x92, 0x54, 0x38, 0x3C, 0x56, 0x93, 0x78, 0x23, 0x82, 0xCD, 0xE0, 0x9C, 0xEB, 0x5C,
|
||||
0xC4, 0x70, 0x27, 0x3A, 0xD7, 0x31, 0x9A, 0xCC, 0xA9, 0x7A, 0x52, 0x94, 0xE4, 0x8F, 0x3D, 0x6D, 0xA6, 0x90, 0x8C, 0x7F, 0x18,
|
||||
0xC4, 0x79, 0x1C, 0x71, 0x45, 0xE0, 0xFC, 0x63, 0x18, 0xC4, 0xFC, 0x63, 0x18, 0xFA, 0x10, 0x80, 0x79, 0x1C, 0x30, 0x45, 0xE0,
|
||||
0xF9, 0x08, 0x42, 0x10, 0x8A, 0x56, 0xA3, 0x10, 0x8C, 0x40, 0x04, 0x01, 0x07, 0xF9, 0x31, 0xC4, 0x71, 0x14, 0xC5, 0xFE, 0x04,
|
||||
0x01, 0x00, 0x40, 0x4B, 0x8C, 0x65, 0xE4, 0x8A, 0x28, 0xA2, 0x8B, 0xF0, 0x40, 0x99, 0x97, 0x11, 0x96, 0x59, 0x65, 0x97, 0xF0,
|
||||
0x95, 0x2A, 0x54, 0xA9, 0x5F, 0xC0, 0x80, 0xF0, 0x20, 0x78, 0x91, 0x23, 0xC0, 0x86, 0x1F, 0x63, 0x8F, 0xD0, 0x84, 0x3D, 0x18,
|
||||
0xF8, 0xF4, 0xDE, 0x19, 0xF8, 0x9E, 0xA2, 0xE1, 0xA1, 0xA2, 0x9E, 0xFC, 0x7E, 0xD4, 0xC4,
|
||||
};
|
||||
|
||||
const GFXglyph FreeSans6pt8bCyrillicGlyphs[] PROGMEM = {
|
||||
{0, 0, 0, 3, 0, 0}, // 0x20 ' '
|
||||
{3, 2, 9, 3, 1, -8}, // 0x21 '!'
|
||||
{6, 3, 3, 4, 1, -8}, // 0x22 '"'
|
||||
{8, 7, 8, 7, 0, -7}, // 0x23 '#'
|
||||
{15, 6, 11, 7, 0, -8}, // 0x24 '$'
|
||||
{24, 10, 9, 11, 0, -8}, // 0x25 '%'
|
||||
{36, 6, 9, 8, 1, -8}, // 0x26 '&'
|
||||
{43, 1, 3, 2, 1, -8}, // 0x27 '''
|
||||
{44, 2, 10, 4, 1, -7}, // 0x28 '('
|
||||
{47, 3, 11, 4, 0, -7}, // 0x29 ')'
|
||||
{52, 3, 4, 5, 1, -8}, // 0x2A '*'
|
||||
{54, 5, 6, 7, 1, -5}, // 0x2B '+'
|
||||
{58, 1, 3, 3, 1, 0}, // 0x2C ','
|
||||
{59, 2, 1, 4, 1, -3}, // 0x2D '-'
|
||||
{60, 1, 1, 3, 1, 0}, // 0x2E '.'
|
||||
{61, 3, 8, 3, 0, -7}, // 0x2F '/'
|
||||
{64, 5, 9, 7, 1, -8}, // 0x30 '0'
|
||||
{70, 3, 9, 7, 1, -8}, // 0x31 '1'
|
||||
{74, 6, 9, 7, 0, -8}, // 0x32 '2'
|
||||
{81, 5, 9, 7, 1, -8}, // 0x33 '3'
|
||||
{87, 6, 9, 7, 0, -8}, // 0x34 '4'
|
||||
{94, 5, 9, 7, 1, -8}, // 0x35 '5'
|
||||
{100, 5, 9, 7, 1, -8}, // 0x36 '6'
|
||||
{106, 5, 9, 7, 1, -8}, // 0x37 '7'
|
||||
{112, 6, 9, 7, 0, -8}, // 0x38 '8'
|
||||
{119, 6, 9, 7, 0, -8}, // 0x39 '9'
|
||||
{126, 2, 6, 3, 1, -5}, // 0x3A ':'
|
||||
{128, 2, 8, 3, 1, -5}, // 0x3B ';'
|
||||
{130, 5, 5, 7, 1, -4}, // 0x3C '<'
|
||||
{134, 5, 3, 7, 1, -3}, // 0x3D '='
|
||||
{136, 5, 5, 7, 1, -4}, // 0x3E '>'
|
||||
{140, 5, 9, 7, 1, -8}, // 0x3F '?'
|
||||
{146, 11, 11, 12, 0, -8}, // 0x40 '@'
|
||||
{162, 8, 9, 8, 0, -8}, // 0x41 'A'
|
||||
{171, 6, 9, 8, 1, -8}, // 0x42 'B'
|
||||
{178, 7, 9, 9, 1, -8}, // 0x43 'C'
|
||||
{186, 7, 9, 9, 1, -8}, // 0x44 'D'
|
||||
{194, 6, 9, 8, 1, -8}, // 0x45 'E'
|
||||
{201, 6, 9, 7, 1, -8}, // 0x46 'F'
|
||||
{208, 8, 9, 9, 1, -8}, // 0x47 'G'
|
||||
{217, 7, 9, 9, 1, -8}, // 0x48 'H'
|
||||
{225, 1, 9, 3, 1, -8}, // 0x49 'I'
|
||||
{227, 5, 9, 6, 0, -8}, // 0x4A 'J'
|
||||
{233, 7, 9, 8, 1, -8}, // 0x4B 'K'
|
||||
{241, 5, 9, 7, 1, -8}, // 0x4C 'L'
|
||||
{247, 8, 9, 10, 1, -8}, // 0x4D 'M'
|
||||
{256, 7, 9, 9, 1, -8}, // 0x4E 'N'
|
||||
{264, 9, 9, 9, 0, -8}, // 0x4F 'O'
|
||||
{275, 6, 9, 8, 1, -8}, // 0x50 'P'
|
||||
{282, 9, 9, 9, 0, -8}, // 0x51 'Q'
|
||||
{293, 7, 9, 9, 1, -8}, // 0x52 'R'
|
||||
{301, 6, 9, 8, 1, -8}, // 0x53 'S'
|
||||
{308, 7, 9, 7, 0, -8}, // 0x54 'T'
|
||||
{316, 7, 9, 9, 1, -8}, // 0x55 'U'
|
||||
{324, 8, 9, 8, 0, -8}, // 0x56 'V'
|
||||
{333, 11, 9, 11, 0, -8}, // 0x57 'W'
|
||||
{346, 6, 9, 8, 1, -8}, // 0x58 'X'
|
||||
{353, 8, 9, 8, 0, -8}, // 0x59 'Y'
|
||||
{362, 7, 9, 7, 0, -8}, // 0x5A 'Z'
|
||||
{370, 2, 12, 3, 1, -8}, // 0x5B '['
|
||||
{373, 3, 9, 3, 0, -8}, // 0x5C '\'
|
||||
{377, 3, 12, 3, 0, -8}, // 0x5D ']'
|
||||
{382, 4, 5, 6, 1, -8}, // 0x5E '^'
|
||||
{385, 6, 1, 7, 0, 2}, // 0x5F '_'
|
||||
{386, 2, 2, 4, 1, -8}, // 0x60 '`'
|
||||
{387, 5, 6, 7, 1, -5}, // 0x61 'a'
|
||||
{391, 5, 9, 7, 1, -8}, // 0x62 'b'
|
||||
{397, 6, 6, 6, 0, -5}, // 0x63 'c'
|
||||
{402, 6, 9, 7, 0, -8}, // 0x64 'd'
|
||||
{409, 5, 6, 7, 1, -5}, // 0x65 'e'
|
||||
{413, 3, 9, 3, 0, -8}, // 0x66 'f'
|
||||
{417, 6, 9, 7, 0, -5}, // 0x67 'g'
|
||||
{424, 5, 9, 7, 1, -8}, // 0x68 'h'
|
||||
{430, 1, 9, 3, 1, -8}, // 0x69 'i'
|
||||
{432, 2, 12, 3, 0, -8}, // 0x6A 'j'
|
||||
{435, 5, 9, 6, 1, -8}, // 0x6B 'k'
|
||||
{441, 1, 9, 3, 1, -8}, // 0x6C 'l'
|
||||
{443, 8, 6, 10, 1, -5}, // 0x6D 'm'
|
||||
{449, 5, 6, 7, 1, -5}, // 0x6E 'n'
|
||||
{453, 6, 6, 7, 0, -5}, // 0x6F 'o'
|
||||
{458, 5, 9, 7, 1, -5}, // 0x70 'p'
|
||||
{464, 6, 9, 7, 0, -5}, // 0x71 'q'
|
||||
{471, 3, 6, 4, 1, -5}, // 0x72 'r'
|
||||
{474, 6, 6, 6, 0, -5}, // 0x73 's'
|
||||
{479, 3, 8, 3, 0, -7}, // 0x74 't'
|
||||
{482, 5, 6, 7, 1, -5}, // 0x75 'u'
|
||||
{486, 6, 6, 6, 0, -5}, // 0x76 'v'
|
||||
{491, 8, 6, 9, 0, -5}, // 0x77 'w'
|
||||
{497, 4, 6, 6, 1, -5}, // 0x78 'x'
|
||||
{500, 5, 9, 6, 0, -5}, // 0x79 'y'
|
||||
{506, 5, 6, 6, 0, -5}, // 0x7A 'z'
|
||||
{510, 2, 12, 4, 1, -8}, // 0x7B '{'
|
||||
{513, 1, 12, 3, 1, -8}, // 0x7C '|'
|
||||
{515, 3, 12, 4, 0, -8}, // 0x7D '}'
|
||||
{520, 5, 2, 7, 1, -4}, // 0x7E '~'
|
||||
{522, 6, 9, 8, 1, -8}, //
|
||||
{529, 9, 11, 9, 0, -8}, //
|
||||
{542, 6, 11, 7, 1, -10}, //
|
||||
{551, 0, 0, 8, 0, 0}, //
|
||||
{551, 4, 9, 5, 1, -8}, //
|
||||
{556, 0, 0, 8, 0, 0}, //
|
||||
{556, 0, 0, 8, 0, 0}, //
|
||||
{556, 0, 0, 8, 0, 0}, //
|
||||
{556, 0, 0, 8, 0, 0}, //
|
||||
{556, 6, 8, 8, 1, -7}, //
|
||||
{562, 0, 0, 8, 0, 0}, //
|
||||
{562, 11, 9, 13, 1, -8}, //
|
||||
{575, 0, 0, 8, 0, 0}, //
|
||||
{575, 11, 9, 12, 1, -8}, //
|
||||
{588, 6, 11, 8, 1, -10}, //
|
||||
{597, 9, 9, 9, 0, -8}, //
|
||||
{608, 7, 11, 9, 1, -8}, //
|
||||
{618, 6, 11, 7, 0, -8}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 0, 0, 8, 0, 0}, //
|
||||
{627, 9, 6, 10, 0, -5}, //
|
||||
{634, 0, 0, 8, 0, 0}, //
|
||||
{634, 9, 6, 10, 1, -5}, //
|
||||
{641, 4, 8, 6, 1, -7}, //
|
||||
{645, 6, 9, 7, 0, -8}, //
|
||||
{652, 5, 7, 7, 1, -5}, //
|
||||
{657, 0, 0, 8, 0, 0}, //
|
||||
{657, 7, 11, 7, 0, -10}, //
|
||||
{667, 5, 11, 6, 0, -7}, //
|
||||
{674, 5, 9, 6, 0, -8}, //
|
||||
{680, 0, 0, 8, 0, 0}, //
|
||||
{680, 6, 10, 7, 1, -9}, //
|
||||
{688, 0, 0, 8, 0, 0}, //
|
||||
{688, 0, 0, 8, 0, 0}, //
|
||||
{688, 6, 11, 8, 1, -10}, //
|
||||
{697, 7, 9, 9, 1, -8}, //
|
||||
{705, 0, 0, 8, 0, 0}, //
|
||||
{705, 0, 0, 8, 0, 0}, //
|
||||
{705, 2, 12, 3, 0, -8}, //
|
||||
{708, 0, 0, 8, 0, 0}, //
|
||||
{708, 0, 0, 8, 0, 0}, //
|
||||
{708, 3, 11, 3, 0, -10}, //
|
||||
{713, 0, 0, 8, 0, 0}, //
|
||||
{713, 0, 0, 8, 0, 0}, //
|
||||
{713, 1, 9, 3, 1, -8}, //
|
||||
{715, 1, 9, 3, 1, -8}, //
|
||||
{717, 3, 8, 5, 1, -7}, //
|
||||
{720, 6, 9, 7, 1, -5}, //
|
||||
{727, 0, 0, 8, 0, 0}, //
|
||||
{727, 0, 0, 8, 0, 0}, //
|
||||
{727, 6, 9, 7, 0, -8}, //
|
||||
{734, 9, 9, 11, 1, -8}, //
|
||||
{745, 6, 6, 6, 0, -5}, //
|
||||
{750, 0, 0, 8, 0, 0}, //
|
||||
{750, 0, 0, 8, 0, 0}, //
|
||||
{750, 6, 9, 8, 1, -8}, //
|
||||
{757, 6, 6, 6, 0, -5}, //
|
||||
{762, 3, 9, 3, 0, -8}, //
|
||||
{766, 8, 9, 8, 0, -8}, //
|
||||
{775, 6, 9, 8, 1, -8}, //
|
||||
{782, 6, 9, 8, 1, -8}, //
|
||||
{789, 6, 9, 7, 1, -8}, //
|
||||
{796, 9, 11, 10, 0, -8}, //
|
||||
{809, 6, 9, 8, 1, -8}, //
|
||||
{816, 9, 9, 11, 1, -8}, //
|
||||
{827, 6, 9, 8, 1, -8}, //
|
||||
{834, 7, 9, 9, 1, -8}, //
|
||||
{842, 7, 11, 9, 1, -10}, //
|
||||
{852, 6, 9, 8, 1, -8}, //
|
||||
{859, 7, 9, 8, 0, -8}, //
|
||||
{867, 8, 9, 10, 1, -8}, //
|
||||
{876, 7, 9, 9, 1, -8}, //
|
||||
{884, 8, 9, 10, 1, -8}, //
|
||||
{893, 7, 9, 9, 1, -8}, //
|
||||
{901, 6, 9, 8, 1, -8}, //
|
||||
{908, 7, 9, 9, 1, -8}, //
|
||||
{916, 7, 9, 7, 0, -8}, //
|
||||
{924, 7, 9, 7, 0, -8}, //
|
||||
{932, 9, 9, 10, 1, -8}, //
|
||||
{943, 6, 9, 8, 1, -8}, //
|
||||
{950, 8, 11, 9, 1, -8}, //
|
||||
{961, 6, 9, 8, 1, -8}, //
|
||||
{968, 8, 9, 10, 1, -8}, //
|
||||
{977, 9, 11, 10, 1, -8}, //
|
||||
{990, 10, 9, 10, 0, -8}, //
|
||||
{1002, 9, 9, 10, 1, -8}, //
|
||||
{1013, 6, 9, 8, 1, -8}, //
|
||||
{1020, 7, 9, 9, 1, -8}, //
|
||||
{1028, 10, 9, 12, 1, -8}, //
|
||||
{1040, 6, 9, 8, 1, -8}, //
|
||||
{1047, 6, 6, 7, 0, -5}, //
|
||||
{1052, 6, 9, 7, 0, -8}, //
|
||||
{1059, 5, 6, 6, 1, -5}, //
|
||||
{1063, 4, 6, 5, 1, -5}, //
|
||||
{1066, 7, 7, 7, 0, -5}, //
|
||||
{1073, 6, 6, 7, 0, -5}, //
|
||||
{1078, 8, 6, 9, 1, -5}, //
|
||||
{1084, 6, 6, 6, 0, -5}, //
|
||||
{1089, 5, 6, 7, 1, -5}, //
|
||||
{1093, 5, 8, 7, 1, -7}, //
|
||||
{1098, 4, 6, 6, 1, -5}, //
|
||||
{1101, 5, 6, 6, 0, -5}, //
|
||||
{1105, 6, 6, 7, 1, -5}, //
|
||||
{1110, 5, 6, 7, 1, -5}, //
|
||||
{1114, 6, 6, 7, 0, -5}, //
|
||||
{1119, 5, 6, 7, 1, -5}, //
|
||||
{1123, 5, 9, 7, 1, -5}, //
|
||||
{1129, 6, 6, 6, 0, -5}, //
|
||||
{1134, 5, 6, 5, 0, -5}, //
|
||||
{1138, 5, 9, 6, 0, -5}, //
|
||||
{1144, 10, 11, 10, 0, -7}, //
|
||||
{1158, 5, 6, 6, 0, -5}, //
|
||||
{1162, 6, 7, 7, 1, -5}, //
|
||||
{1168, 4, 6, 6, 1, -5}, //
|
||||
{1171, 6, 6, 8, 1, -5}, //
|
||||
{1176, 7, 7, 9, 1, -5}, //
|
||||
{1183, 7, 6, 8, 0, -5}, //
|
||||
{1189, 6, 6, 8, 1, -5}, //
|
||||
{1194, 5, 6, 6, 1, -5}, //
|
||||
{1198, 5, 6, 6, 1, -5}, //
|
||||
{1202, 8, 6, 9, 1, -5}, //
|
||||
{1208, 5, 6, 7, 1, -5} //
|
||||
};
|
||||
|
||||
const GFXfont FreeSans6pt8bCyrillic PROGMEM = {(uint8_t *)FreeSans6pt8bCyrillicBitmaps, (GFXglyph *)FreeSans6pt8bCyrillicGlyphs,
|
||||
0x20, 0xFF, 16};
|
|
@ -0,0 +1,4 @@
|
|||
# NicheGraphics - Fonts
|
||||
|
||||
A common area to store fonts which might be reused by different Niche Graphics UIs
|
||||
In future, we may want to separate these by library (AdafruitGFX, u8g2, etc)
|
|
@ -0,0 +1,843 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./Applet.h"
|
||||
|
||||
#include "RTC.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::AppletFont InkHUD::Applet::fontLarge; // General purpose font. Set by setDefaultFonts
|
||||
InkHUD::AppletFont InkHUD::Applet::fontSmall; // General purpose font. Set by setDefaultFonts
|
||||
constexpr float InkHUD::Applet::LOGO_ASPECT_RATIO; // Ratio of the Meshtastic logo
|
||||
|
||||
InkHUD::Applet::Applet() : GFX(0, 0)
|
||||
{
|
||||
// GFX is given initial dimensions of 0
|
||||
// The width and height will change dynamically, depending on Applet tiling
|
||||
// If you're getting a "divide by zero error", consider it an assert:
|
||||
// WindowManager should be the only one controlling the rendering
|
||||
}
|
||||
|
||||
// The raw pixel output generated by AdafruitGFX drawing
|
||||
// Hand off to the applet's tile, which will in-turn pass to the window manager
|
||||
void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color)
|
||||
{
|
||||
// Only render pixels if they fall within user's cropped region
|
||||
if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight))
|
||||
assignedTile->handleAppletPixel(x, y, (Color)color);
|
||||
}
|
||||
|
||||
// Sets which tile the applet renders for
|
||||
// Pixel output is passed to tile during render()
|
||||
// This should only be called by Tile::assignApplet
|
||||
void InkHUD::Applet::setTile(Tile *t)
|
||||
{
|
||||
// If we're setting (not clearing), make sure the link is "reciprocal"
|
||||
if (t)
|
||||
assert(t->getAssignedApplet() == this);
|
||||
|
||||
assignedTile = t;
|
||||
}
|
||||
|
||||
// Which tile will the applet render() to?
|
||||
InkHUD::Tile *InkHUD::Applet::getTile()
|
||||
{
|
||||
return assignedTile;
|
||||
}
|
||||
|
||||
void InkHUD::Applet::render()
|
||||
{
|
||||
assert(assignedTile); // Ensure that we have a tile
|
||||
assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile
|
||||
|
||||
wantRender = false; // Clear the flag set by requestUpdate
|
||||
wantAutoshow = false; // If we're rendering now, it means our request was considered. It may or may not have been granted.
|
||||
wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Our requested type has been considered by now. Tidy up.
|
||||
|
||||
updateDimensions();
|
||||
resetDrawingSpace();
|
||||
onRender(); // Derived applet's drawing takes place here
|
||||
|
||||
// If our tile is (or was) highlighted, to indicate a change in focus
|
||||
if (Tile::highlightTarget == assignedTile) {
|
||||
// Draw the highlight
|
||||
if (!Tile::highlightShown) {
|
||||
drawRect(0, 0, width(), height(), BLACK);
|
||||
Tile::startHighlightTimeout();
|
||||
Tile::highlightShown = true;
|
||||
}
|
||||
|
||||
// Clear the highlight
|
||||
else {
|
||||
Tile::cancelHighlightTimeout();
|
||||
Tile::highlightShown = false;
|
||||
Tile::highlightTarget = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Does the applet want to render now?
|
||||
// Checks whether the applet called requestUpdate() recently, in response to an event
|
||||
bool InkHUD::Applet::wantsToRender()
|
||||
{
|
||||
return wantRender;
|
||||
}
|
||||
|
||||
// Does the applet want to be moved to foreground before next render, to show new data?
|
||||
// User specifies whether an applet has permission for this, using the on-screen menu
|
||||
bool InkHUD::Applet::wantsToAutoshow()
|
||||
{
|
||||
return wantAutoshow;
|
||||
}
|
||||
|
||||
// Which technique would this applet prefer that the display use to change the image?
|
||||
Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType()
|
||||
{
|
||||
return wantUpdateType;
|
||||
}
|
||||
|
||||
// Get size of the applet's drawing space from its tile
|
||||
void InkHUD::Applet::updateDimensions()
|
||||
{
|
||||
assert(assignedTile);
|
||||
WIDTH = assignedTile->getWidth();
|
||||
HEIGHT = assignedTile->getHeight();
|
||||
_width = WIDTH;
|
||||
_height = HEIGHT;
|
||||
}
|
||||
|
||||
// Ensure that render() always starts with the same initial drawing config
|
||||
void InkHUD::Applet::resetDrawingSpace()
|
||||
{
|
||||
resetCrop(); // Allow pixel from any region of the applet to draw
|
||||
setTextColor(BLACK); // Reset text params
|
||||
setCursor(0, 0);
|
||||
setTextWrap(false);
|
||||
setFont(AppletFont()); // Restore the default AdafruitGFX font
|
||||
}
|
||||
|
||||
// Tell the window manager that we want to render now
|
||||
// Applets should internally listen for events they are interested in, via MeshModule, CallbackObserver etc
|
||||
// When an applet decides it has heard something important, and wants to redraw, it calls this method
|
||||
// Once the window manager 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 (forgeround)
|
||||
void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type)
|
||||
{
|
||||
wantRender = true;
|
||||
wantUpdateType = type;
|
||||
WindowManager::getInstance()->requestUpdate();
|
||||
}
|
||||
|
||||
// Ask window manager to move this applet to foreground at start of next render
|
||||
// Users select which applets have permission for this using the on-screen menu
|
||||
void InkHUD::Applet::requestAutoshow()
|
||||
{
|
||||
wantAutoshow = true;
|
||||
}
|
||||
|
||||
// Called when an Applet begins running
|
||||
// Active applets are considered "enabled"
|
||||
// They should now listen for events, and request their own updates
|
||||
// They may also be force rendered by the window manager at any time
|
||||
// Applets can be activated at run-time through the on-screen menu
|
||||
void InkHUD::Applet::activate()
|
||||
{
|
||||
onActivate(); // Call derived class' handler
|
||||
active = true;
|
||||
}
|
||||
|
||||
// Called when an Applet stop running
|
||||
// Inactive applets are considered "disabled"
|
||||
// They should not listen for events, process data
|
||||
// They will not be rendered
|
||||
// Applets can be deactivated at run-time through the on-screen menu
|
||||
void InkHUD::Applet::deactivate()
|
||||
{
|
||||
// If applet is still in foreground, run its onBackground code first
|
||||
if (isForeground())
|
||||
sendToBackground();
|
||||
|
||||
// If applet is active, run its onDeactivate code first
|
||||
if (isActive())
|
||||
onDeactivate(); // Derived class' handler
|
||||
active = false;
|
||||
}
|
||||
|
||||
// Is the Applet running?
|
||||
// Note: active / inactive is not related to background / foreground
|
||||
// An inactive applet is *fully* disabled
|
||||
bool InkHUD::Applet::isActive()
|
||||
{
|
||||
return active;
|
||||
}
|
||||
|
||||
// Begin showing the Applet
|
||||
// It will be rendered immediately to whichever tile it is assigned
|
||||
// The window manager will also now honor requestUpdate() calls from this applet
|
||||
void InkHUD::Applet::bringToForeground()
|
||||
{
|
||||
if (!foreground) {
|
||||
foreground = true;
|
||||
onForeground(); // Run derived applet class' handler
|
||||
}
|
||||
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
// Stop showing the Applet
|
||||
// Calls to requestUpdate() will no longer be honored
|
||||
// When one applet moves to background, another should move to foreground
|
||||
void InkHUD::Applet::sendToBackground()
|
||||
{
|
||||
if (foreground) {
|
||||
foreground = false;
|
||||
onBackground(); // Run derived applet class' handler
|
||||
}
|
||||
}
|
||||
|
||||
// Is the applet currently displayed on a tile
|
||||
bool InkHUD::Applet::isForeground()
|
||||
{
|
||||
return foreground;
|
||||
}
|
||||
|
||||
// Limit drawing to a certain region of the applet
|
||||
// Pixels outside this region will be discarded
|
||||
void InkHUD::Applet::setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height)
|
||||
{
|
||||
cropLeft = left;
|
||||
cropTop = top;
|
||||
cropWidth = width;
|
||||
cropHeight = height;
|
||||
}
|
||||
|
||||
// Allow drawing to any region of the Applet
|
||||
// Reverses Applet::setCrop
|
||||
void InkHUD::Applet::resetCrop()
|
||||
{
|
||||
setCrop(0, 0, width(), height());
|
||||
}
|
||||
|
||||
// Convert relative width to absolute width, in px
|
||||
// X(0) is 0
|
||||
// X(0.5) is width() / 2
|
||||
// X(1) is width()
|
||||
uint16_t InkHUD::Applet::X(float f)
|
||||
{
|
||||
return width() * f;
|
||||
}
|
||||
|
||||
// Convert relative hight to absolute height, in px
|
||||
// Y(0) is 0
|
||||
// Y(0.5) is height() / 2
|
||||
// Y(1) is height()
|
||||
uint16_t InkHUD::Applet::Y(float f)
|
||||
{
|
||||
return height() * f;
|
||||
}
|
||||
|
||||
// Print text, specifying the position of any edge / corner of the textbox
|
||||
void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha, VerticalAlignment va)
|
||||
{
|
||||
printAt(x, y, std::string(text), ha, va);
|
||||
}
|
||||
|
||||
// Print text, specifying the position of any edge / corner of the textbox
|
||||
void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va)
|
||||
{
|
||||
// Custom font
|
||||
// - set with AppletFont::addSubstitution
|
||||
// - find certain UTF8 chars
|
||||
// - replace with glpyh from custom font (or suitable ASCII addSubstitution?)
|
||||
getFont().applySubstitutions(&text);
|
||||
|
||||
// We do still have to run getTextBounds to find the width
|
||||
int16_t textOffsetX, textOffsetY;
|
||||
uint16_t textWidth, textHeight;
|
||||
getTextBounds(text.c_str(), 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight);
|
||||
|
||||
int16_t cursorX = 0;
|
||||
int16_t cursorY = 0;
|
||||
|
||||
switch (ha) {
|
||||
case LEFT:
|
||||
cursorX = x - textOffsetX;
|
||||
break;
|
||||
case CENTER:
|
||||
cursorX = (x - textOffsetX) - (textWidth / 2);
|
||||
break;
|
||||
case RIGHT:
|
||||
cursorX = (x - textOffsetX) - textWidth;
|
||||
break;
|
||||
}
|
||||
|
||||
// We're using a fixed line height (getFontDimensions), rather than sizing to text (getTextBounds)
|
||||
// Note: the FontDimensions values for this are unsigned
|
||||
|
||||
switch (va) {
|
||||
case TOP:
|
||||
cursorY = y + currentFont.heightAboveCursor();
|
||||
break;
|
||||
case MIDDLE:
|
||||
cursorY = (y + currentFont.heightAboveCursor()) - (currentFont.lineHeight() / 2);
|
||||
break;
|
||||
case BOTTOM:
|
||||
cursorY = (y + currentFont.heightAboveCursor()) - currentFont.lineHeight();
|
||||
break;
|
||||
}
|
||||
|
||||
setCursor(cursorX, cursorY);
|
||||
print(text.c_str());
|
||||
}
|
||||
|
||||
// Set which font should be used for subsequent drawing
|
||||
// This is AppletFont type, which is a wrapper for AdfruitGFX font, with some precalculated dimension data
|
||||
void InkHUD::Applet::setFont(AppletFont f)
|
||||
{
|
||||
GFX::setFont(f.gfxFont);
|
||||
currentFont = f;
|
||||
}
|
||||
|
||||
// Get which font is currently being used for drawing
|
||||
// This is AppletFont type, which is a wrapper for AdfruitGFX font, with some precalculated dimension data
|
||||
InkHUD::AppletFont InkHUD::Applet::getFont()
|
||||
{
|
||||
return currentFont;
|
||||
}
|
||||
|
||||
// Set two general-purpose fonts, which are reused by many applets
|
||||
// Applets are also permitted to use other fonts, if they can justify the flash usage
|
||||
void InkHUD::Applet::setDefaultFonts(AppletFont large, AppletFont small)
|
||||
{
|
||||
Applet::fontSmall = small;
|
||||
Applet::fontLarge = large;
|
||||
}
|
||||
|
||||
// Gets rendered width of a string
|
||||
// Wrapper for getTextBounds
|
||||
uint16_t InkHUD::Applet::getTextWidth(const char *text)
|
||||
{
|
||||
|
||||
// We do still have to run getTextBounds to find the width
|
||||
int16_t textOffsetX, textOffsetY;
|
||||
uint16_t textWidth, textHeight;
|
||||
getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight);
|
||||
|
||||
return textWidth;
|
||||
}
|
||||
|
||||
// Gets rendered width of a string
|
||||
// Wrappe for getTextBounds
|
||||
uint16_t InkHUD::Applet::getTextWidth(std::string text)
|
||||
{
|
||||
getFont().applySubstitutions(&text);
|
||||
|
||||
return getTextWidth(text.c_str());
|
||||
}
|
||||
|
||||
// Evaluate SNR and RSSI to qualify signal strength at one of four discrete levels
|
||||
// Roughly comparable to values used by the iOS app;
|
||||
// I didn't actually go look up the code, just fit to a sample graphic I have of the iOS signal indicator
|
||||
InkHUD::SignalStrength InkHUD::Applet::getSignalStrength(float snr, float rssi)
|
||||
{
|
||||
uint8_t score = 0;
|
||||
|
||||
// Give a score for the SNR
|
||||
if (snr > -17.5)
|
||||
score += 2;
|
||||
else if (snr > -26.0)
|
||||
score += 1;
|
||||
|
||||
// Give a score for the RSSI
|
||||
if (rssi > -115.0)
|
||||
score += 3;
|
||||
else if (rssi > -120.0)
|
||||
score += 2;
|
||||
else if (rssi > -126.0)
|
||||
score += 1;
|
||||
|
||||
// Combine scores, then give a result
|
||||
if (score >= 5)
|
||||
return SIGNAL_GOOD;
|
||||
else if (score >= 4)
|
||||
return SIGNAL_FAIR;
|
||||
else if (score > 0)
|
||||
return SIGNAL_BAD;
|
||||
else
|
||||
return SIGNAL_NONE;
|
||||
}
|
||||
|
||||
// Apply the standard "node id" formatting to a nodenum int: !0123abdc
|
||||
std::string InkHUD::Applet::hexifyNodeNum(NodeNum num)
|
||||
{
|
||||
// Not found in nodeDB, show a hex nodeid instead
|
||||
char nodeIdHex[10];
|
||||
sprintf(nodeIdHex, "!%0x", num); // Convert to the typical "fixed width hex with !" format
|
||||
return std::string(nodeIdHex);
|
||||
}
|
||||
|
||||
void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text)
|
||||
{
|
||||
// Custom font glyphs
|
||||
// - set with AppletFont::addSubstitution
|
||||
// - find certain UTF8 chars
|
||||
// - replace with glpyh from custom font (or suitable ASCII addSubstitution?)
|
||||
getFont().applySubstitutions(&text);
|
||||
|
||||
// Place the AdafruitGFX cursor to suit our "top" coord
|
||||
setCursor(left, top + getFont().heightAboveCursor());
|
||||
|
||||
// How wide a space character is
|
||||
// Used when simulating print, for dimensioning
|
||||
// Works around issues where getTextDimensions() doesn't account for whitespace
|
||||
const uint8_t wSp = getFont().widthBetweenWords();
|
||||
|
||||
// Move through our text, character by character
|
||||
uint16_t wordStart = 0;
|
||||
for (uint16_t i = 0; i < text.length(); i++) {
|
||||
|
||||
// Found: end of word (split by spaces or newline)
|
||||
// Also handles end of string
|
||||
if (text[i] == ' ' || text[i] == '\n' || i == text.length() - 1) {
|
||||
// Isolate this word
|
||||
uint16_t wordLength = (i - wordStart) + 1; // Plus one. Imagine: "a". End - Start is 0, but length is 1
|
||||
std::string word = text.substr(wordStart, wordLength);
|
||||
wordStart = i + 1; // Next word starts *after* the space
|
||||
|
||||
// If word is terminated by a newline char, don't actually print it.
|
||||
// We'll manually add a new line later
|
||||
if (word.back() == '\n')
|
||||
word.pop_back();
|
||||
|
||||
// Measure the word, in px
|
||||
int16_t l, t;
|
||||
uint16_t w, h;
|
||||
getTextBounds(word.c_str(), getCursorX(), getCursorY(), &l, &t, &w, &h);
|
||||
|
||||
// Word is short
|
||||
if (w < width) {
|
||||
// Word fits on current line
|
||||
if ((l + w + wSp) < left + width)
|
||||
print(word.c_str());
|
||||
|
||||
// Word doesn't fit on current line
|
||||
else {
|
||||
setCursor(left, getCursorY() + getFont().lineHeight()); // Newline
|
||||
print(word.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Word is really long
|
||||
// (wider than applet)
|
||||
else {
|
||||
// Horribly inefficient:
|
||||
// Rather than working directly with the glyph sizes,
|
||||
// we're going to run everything through getTextBounds as a c-string of length 1
|
||||
// This is because AdafruitGFX has special internal handling for their legacy 6x8 font,
|
||||
// which would be a pain to add manually here.
|
||||
// These super-long strings probably don't come up often so we can maybe tolerate this.
|
||||
|
||||
// Todo: rewrite making use of AdafruitGFX native text wrapping
|
||||
char cstr[] = {0, 0};
|
||||
int16_t l, t;
|
||||
uint16_t w, h;
|
||||
for (uint16_t c = 0; c < word.length(); c++) {
|
||||
// Shove next char into a c string
|
||||
cstr[0] = word[c];
|
||||
getTextBounds(cstr, getCursorX(), getCursorY(), &l, &t, &w, &h);
|
||||
|
||||
// Manual newline, if next character will spill beyond screen edge
|
||||
if ((l + w) > left + width)
|
||||
setCursor(left, getCursorY() + getFont().lineHeight());
|
||||
|
||||
// Print next character
|
||||
print(word[c]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If word was terminated by a newline char, manually add the new line now
|
||||
if (text[i] == '\n') {
|
||||
setCursor(left, getCursorY() + getFont().lineHeight()); // Manual newline
|
||||
wordStart = i + 1; // New word begins after the newline. Otherwise print will add an *extra* line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate running printWrapped, to determine how tall the block of text will be.
|
||||
// This is a wasteful way of handling things. Maybe some way to optimize in future?
|
||||
uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, std::string text)
|
||||
{
|
||||
// Cache the current crop region
|
||||
int16_t cL = cropLeft;
|
||||
int16_t cT = cropTop;
|
||||
uint16_t cW = cropWidth;
|
||||
uint16_t cH = cropHeight;
|
||||
|
||||
setCrop(-1, -1, 0, 0); // Set crop to temporarily discard all pixels
|
||||
printWrapped(left, 0, width, text); // Simulate only - no pixels drawn
|
||||
|
||||
// Restore previous crop region
|
||||
cropLeft = cL;
|
||||
cropTop = cT;
|
||||
cropWidth = cW;
|
||||
cropHeight = cH;
|
||||
|
||||
// Note: printWrapped() offsets the initial cursor position by heightAboveCursor() val,
|
||||
// so we need to account for that when determining the height
|
||||
return (getCursorY() + getFont().heightBelowCursor());
|
||||
}
|
||||
|
||||
// Fill a region with sparse diagonal lines, to create a pseudo-translucent fill
|
||||
void InkHUD::Applet::hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color)
|
||||
{
|
||||
// Cache the currently cropped region
|
||||
int16_t oldCropL = cropLeft;
|
||||
int16_t oldCropT = cropTop;
|
||||
uint16_t oldCropW = cropWidth;
|
||||
uint16_t oldCropH = cropHeight;
|
||||
|
||||
setCrop(x, y, w, h);
|
||||
|
||||
// Draw lines starting along the top edge, every few px
|
||||
for (int16_t ix = x; ix < x + w; ix += spacing) {
|
||||
for (int16_t i = 0; i < w || i < h; i++) {
|
||||
drawPixel(ix + i, y + i, color);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw lines starting along the left edge, every few px
|
||||
for (int16_t iy = y; iy < y + h; iy += spacing) {
|
||||
for (int16_t i = 0; i < w || i < h; i++) {
|
||||
drawPixel(x + i, iy + i, color);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore any previous crop
|
||||
// If none was set, this will clear
|
||||
cropLeft = oldCropL;
|
||||
cropTop = oldCropT;
|
||||
cropWidth = oldCropW;
|
||||
cropHeight = oldCropH;
|
||||
}
|
||||
|
||||
// Get a human readable time representation of an epoch time (seconds since 1970)
|
||||
// If time is invalid, this will be an empty string
|
||||
std::string InkHUD::Applet::getTimeString(uint32_t epochSeconds)
|
||||
{
|
||||
#ifdef BUILD_EPOCH
|
||||
constexpr uint32_t validAfterEpoch = BUILD_EPOCH - (SEC_PER_DAY * 30 * 6); // 6 Months prior to build
|
||||
#else
|
||||
constexpr uint32_t validAfterEpoch = 1727740800 - (SEC_PER_DAY * 30 * 6); // 6 Months prior to October 1, 2024 12:00:00 AM GMT
|
||||
#endif
|
||||
|
||||
uint32_t epochNow = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
|
||||
int32_t daysAgo = (epochNow - epochSeconds) / SEC_PER_DAY;
|
||||
int32_t hoursAgo = (epochNow - epochSeconds) / SEC_PER_HOUR;
|
||||
|
||||
// Times are invalid: rtc is much older than when code was built
|
||||
// Don't give any human readable string
|
||||
if (epochNow <= validAfterEpoch) {
|
||||
LOG_DEBUG("RTC prior to buildtime");
|
||||
return "";
|
||||
}
|
||||
|
||||
// Times are invalid: argument time is significantly ahead of RTC
|
||||
// Don't give any human readable string
|
||||
if (daysAgo < -2) {
|
||||
LOG_DEBUG("RTC in future");
|
||||
return "";
|
||||
}
|
||||
|
||||
// Times are probably invalid: more than 6 months ago
|
||||
if (daysAgo > 6 * 30) {
|
||||
LOG_DEBUG("RTC val > 6 months old");
|
||||
return "";
|
||||
}
|
||||
|
||||
if (daysAgo > 1)
|
||||
return to_string(daysAgo) + " days ago";
|
||||
|
||||
else if (hoursAgo > 18)
|
||||
return "Yesterday";
|
||||
|
||||
else {
|
||||
|
||||
uint32_t hms = epochSeconds % SEC_PER_DAY;
|
||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
|
||||
// Tear apart hms into h:m
|
||||
uint32_t hour = hms / SEC_PER_HOUR;
|
||||
uint32_t min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
|
||||
// Format the clock string
|
||||
char clockStr[11];
|
||||
sprintf(clockStr, "%u:%02u %s", (hour % 12 == 0 ? 12 : hour % 12), min, hour > 11 ? "PM" : "AM");
|
||||
|
||||
return clockStr;
|
||||
}
|
||||
}
|
||||
|
||||
// If no argument specified, get time string for the current RTC time
|
||||
std::string InkHUD::Applet::getTimeString()
|
||||
{
|
||||
return getTimeString(getValidTime(RTCQuality::RTCQualityDevice, true));
|
||||
}
|
||||
|
||||
// Calculate how many nodes have been seen within our preferred window of activity
|
||||
// This period is set by user, via the menu
|
||||
// Todo: optimize to calculate once only per WindowManager::render
|
||||
uint16_t InkHUD::Applet::getActiveNodeCount()
|
||||
{
|
||||
// Don't even try to count nodes if RTC isn't set
|
||||
// The last heard values in nodedb will be incomprehensible
|
||||
if (getRTCQuality() == RTCQualityNone)
|
||||
return 0;
|
||||
|
||||
uint16_t count = 0;
|
||||
|
||||
// For each node in db
|
||||
for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
// Check if heard recently, and not our own node
|
||||
if (sinceLastSeen(node) < settings.recentlyActiveSeconds && node->num != nodeDB->getNodeNum())
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
// Get an abbreviated, human readable, distance string
|
||||
// Honors config.display.units, to offer both metric and imperial
|
||||
std::string InkHUD::Applet::localizeDistance(uint32_t meters)
|
||||
{
|
||||
constexpr float FEET_PER_METER = 3.28084;
|
||||
constexpr uint16_t FEET_PER_MILE = 5280;
|
||||
|
||||
// Resulting string
|
||||
std::string localized;
|
||||
|
||||
// Imeperial
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
uint32_t feet = meters * FEET_PER_METER;
|
||||
// Distant (miles, rounded)
|
||||
if (feet > FEET_PER_MILE / 2) {
|
||||
localized += to_string((uint32_t)roundf(feet / FEET_PER_MILE));
|
||||
localized += "mi";
|
||||
}
|
||||
// Nearby (feet)
|
||||
else {
|
||||
localized += to_string(feet);
|
||||
localized += "ft";
|
||||
}
|
||||
}
|
||||
|
||||
// Metric
|
||||
else {
|
||||
// Distant (kilometers, rounded)
|
||||
if (meters >= 500) {
|
||||
localized += to_string((uint32_t)roundf(meters / 1000.0));
|
||||
localized += "km";
|
||||
}
|
||||
// Nearby (meters)
|
||||
else {
|
||||
localized += to_string(meters);
|
||||
localized += "m";
|
||||
}
|
||||
}
|
||||
|
||||
return localized;
|
||||
}
|
||||
|
||||
void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY)
|
||||
{
|
||||
// How many times to draw along x axis
|
||||
int16_t xStart;
|
||||
int16_t xEnd;
|
||||
switch (thicknessX) {
|
||||
case 0:
|
||||
assert(false);
|
||||
case 1:
|
||||
xStart = xCenter;
|
||||
xEnd = xCenter;
|
||||
break;
|
||||
case 2:
|
||||
xStart = xCenter;
|
||||
xEnd = xCenter + 1;
|
||||
break;
|
||||
default:
|
||||
xStart = xCenter - (thicknessX / 2);
|
||||
xEnd = xCenter + (thicknessX / 2);
|
||||
}
|
||||
|
||||
// How many times to draw along Y axis
|
||||
int16_t yStart;
|
||||
int16_t yEnd;
|
||||
switch (thicknessY) {
|
||||
case 0:
|
||||
assert(false);
|
||||
case 1:
|
||||
yStart = yCenter;
|
||||
yEnd = yCenter;
|
||||
break;
|
||||
case 2:
|
||||
yStart = yCenter;
|
||||
yEnd = yCenter + 1;
|
||||
break;
|
||||
default:
|
||||
yStart = yCenter - (thicknessY / 2);
|
||||
yEnd = yCenter + (thicknessY / 2);
|
||||
}
|
||||
|
||||
// Print multiple times, overlapping
|
||||
for (int16_t x = xStart; x <= xEnd; x++) {
|
||||
for (int16_t y = yStart; y <= yEnd; y++) {
|
||||
printAt(x, y, text, CENTER, MIDDLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allow this applet to suppress notifications
|
||||
// Asked 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::Applet::approveNotification(InkHUD::Notification &n)
|
||||
{
|
||||
// By default, no objection
|
||||
return true;
|
||||
}
|
||||
|
||||
// Draw the standard header, used by most Applets
|
||||
void InkHUD::Applet::drawHeader(std::string text)
|
||||
{
|
||||
setFont(fontSmall);
|
||||
|
||||
// Y position for divider
|
||||
// - between header text and messages
|
||||
constexpr int16_t padDivH = 2;
|
||||
const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1;
|
||||
|
||||
// Print header
|
||||
printAt(0, padDivH, text);
|
||||
|
||||
// Divider
|
||||
// - below header text: separates message
|
||||
// - above header text: separates other applets
|
||||
for (int16_t x = 0; x < width(); x += 2) {
|
||||
drawPixel(x, 0, BLACK);
|
||||
drawPixel(x, headerDivY, BLACK); // Dotted 50%
|
||||
}
|
||||
}
|
||||
|
||||
// Get the height of the standard applet header
|
||||
// This will vary, depending on font
|
||||
// Applets use this value to avoid drawing overtop the header
|
||||
uint16_t InkHUD::Applet::getHeaderHeight()
|
||||
{
|
||||
// Y position for divider
|
||||
// - between header text and messages
|
||||
constexpr int16_t padDivH = 2;
|
||||
const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1;
|
||||
|
||||
return headerDivY + 1; // "Plus one": height is always one more than Y position
|
||||
}
|
||||
|
||||
// "Scale to fit": width of Meshtastic logo to fit given region, maintaining aspect ratio
|
||||
uint16_t InkHUD::Applet::getLogoWidth(uint16_t limitWidth, uint16_t limitHeight)
|
||||
{
|
||||
// Determine whether we're limited by width or height
|
||||
// Makes sure we draw the logo as large as possible, within the specified region,
|
||||
// while still maintaining correct aspect ratio
|
||||
if (limitWidth > limitHeight * LOGO_ASPECT_RATIO)
|
||||
return limitHeight * LOGO_ASPECT_RATIO;
|
||||
else
|
||||
return limitWidth;
|
||||
}
|
||||
|
||||
// "Scale to fit": height of Meshtastic logo to fit given region, maintaining aspect ratio
|
||||
uint16_t InkHUD::Applet::getLogoHeight(uint16_t limitWidth, uint16_t limitHeight)
|
||||
{
|
||||
// Determine whether we're limited by width or height
|
||||
// Makes sure we draw the logo as large as possible, within the specified region,
|
||||
// while still maintaining correct aspect ratio
|
||||
if (limitHeight > limitWidth / LOGO_ASPECT_RATIO)
|
||||
return limitWidth / LOGO_ASPECT_RATIO;
|
||||
else
|
||||
return limitHeight;
|
||||
}
|
||||
|
||||
// Draw a scalable Meshtastic logo
|
||||
// Make sure to provide dimensions which have the correct aspect ratio (~2)
|
||||
// Three paths, drawn thick using quads, with one corner "radiused"
|
||||
void InkHUD::Applet::drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height)
|
||||
{
|
||||
struct Point {
|
||||
int x;
|
||||
int y;
|
||||
};
|
||||
typedef Point Distance;
|
||||
|
||||
int16_t logoTh = width * 0.068; // Thickness scales with width. Measured from logo at meshtastic.org.
|
||||
int16_t logoL = centerX - (width / 2) + (logoTh / 2);
|
||||
int16_t logoT = centerY - (height / 2) + (logoTh / 2);
|
||||
int16_t logoW = width - logoTh;
|
||||
int16_t logoH = height - logoTh;
|
||||
int16_t logoR = logoL + logoW - 1;
|
||||
int16_t logoB = logoT + logoH - 1;
|
||||
|
||||
// Points for paths (a, b, and c)
|
||||
Point a1 = {map(0, 0, 3, logoL, logoR), logoB};
|
||||
Point a2 = {map(1, 0, 3, logoL, logoR), logoT};
|
||||
Point b1 = {map(1, 0, 3, logoL, logoR), logoB};
|
||||
Point b2 = {map(2, 0, 3, logoL, logoR), logoT};
|
||||
Point c1 = {map(2, 0, 3, logoL, logoR), logoT};
|
||||
Point c2 = {map(3, 0, 3, logoL, logoR), logoB};
|
||||
|
||||
// Find right-angle to the path
|
||||
// Used to thicken the single pixel paths
|
||||
Distance deltaA = {abs(a2.x - a1.x), abs(a2.y - a1.y)};
|
||||
float angle = tanh((float)deltaA.y / deltaA.x);
|
||||
|
||||
// Distance {at right angle from the paths), which will give corners for our "quads"
|
||||
// The distance is unsigned. We will vary the signedness of the x and y components to suit the path and corner
|
||||
Distance fromPath;
|
||||
fromPath.x = cos(radians(90) - angle) * logoTh * 0.5;
|
||||
fromPath.y = sin(radians(90) - angle) * logoTh * 0.5;
|
||||
|
||||
// Make the path thick: path a becomes quad a
|
||||
Point aq1{a1.x - fromPath.x, a1.y - fromPath.y};
|
||||
Point aq2{a2.x - fromPath.x, a2.y - fromPath.y};
|
||||
Point aq3{a2.x + fromPath.x, a2.y + fromPath.y};
|
||||
Point aq4{a1.x + fromPath.x, a1.y + fromPath.y};
|
||||
fillTriangle(aq1.x, aq1.y, aq2.x, aq2.y, aq3.x, aq3.y, BLACK);
|
||||
fillTriangle(aq1.x, aq1.y, aq3.x, aq3.y, aq4.x, aq4.y, BLACK);
|
||||
|
||||
// Make the path thick: path b becomes quad b
|
||||
Point bq1{b1.x - fromPath.x, b1.y - fromPath.y};
|
||||
Point bq2{b2.x - fromPath.x, b2.y - fromPath.y};
|
||||
Point bq3{b2.x + fromPath.x, b2.y + fromPath.y};
|
||||
Point bq4{b1.x + fromPath.x, b1.y + fromPath.y};
|
||||
fillTriangle(bq1.x, bq1.y, bq2.x, bq2.y, bq3.x, bq3.y, BLACK);
|
||||
fillTriangle(bq1.x, bq1.y, bq3.x, bq3.y, bq4.x, bq4.y, BLACK);
|
||||
|
||||
// Make the path hick: path c becomes quad c
|
||||
Point cq1{c1.x - fromPath.x, c1.y + fromPath.y};
|
||||
Point cq2{c2.x - fromPath.x, c2.y + fromPath.y};
|
||||
Point cq3{c2.x + fromPath.x, c2.y - fromPath.y};
|
||||
Point cq4{c1.x + fromPath.x, c1.y - fromPath.y};
|
||||
fillTriangle(cq1.x, cq1.y, cq2.x, cq2.y, cq3.x, cq3.y, BLACK);
|
||||
fillTriangle(cq1.x, cq1.y, cq3.x, cq3.y, cq4.x, cq4.y, BLACK);
|
||||
|
||||
// Radius the intersection of quad b and quad c
|
||||
// Don't attempt if logo is tiny
|
||||
if (logoTh > 3) {
|
||||
// The radius for the cap *should* be the same as logoTh, but it's not, due to accumulated rounding
|
||||
// We get better results just rederiving it
|
||||
int16_t capRad = sqrt(pow(fromPath.x, 2) + pow(fromPath.y, 2));
|
||||
fillCircle(b2.x, b2.y, capRad, BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,234 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Base class for InkHUD applets
|
||||
Must be overriden
|
||||
|
||||
An applet is one "program" which may show info on the display.
|
||||
|
||||
===================================
|
||||
Preliminary notes, for the curious
|
||||
===================================
|
||||
|
||||
(This info to be streamlined, and moved to a more official documentation)
|
||||
|
||||
User Applets vs System Applets
|
||||
-------------------------------
|
||||
|
||||
There are either "User Applets", or "System Applets".
|
||||
This concept is only for our understanding; as far at the code is concerned, both are just "Applets"
|
||||
|
||||
User applets are the "normal" applets.
|
||||
User applets are applets like "AllMessageApplet", or "MapApplet".
|
||||
User applets may be enabled / disabled by user, via the on-screen menu.
|
||||
Incorporating new UserApplets is easy: just add them during setupNicheGraphics
|
||||
If a UserApplet is not added during setupNicheGraphics, it will not be built.
|
||||
The set of available UserApplets is allowed to vary from device to device.
|
||||
|
||||
|
||||
Examples of system applets include "NotificationApplet" and "MenuApplet".
|
||||
For their own reasons, system applets each require some amount of special handling.
|
||||
|
||||
Drawing
|
||||
--------
|
||||
|
||||
*All* drawing must be performed by an Applet.
|
||||
Applets implement the onRender() method, where all drawing takes place.
|
||||
Applets are told how wide and tall they are, and are expected to draw to suit this size.
|
||||
When an applet draws, it uses co-ordinates in "Applet Space": between 0 and applet width/height.
|
||||
|
||||
Event-driven rendering
|
||||
-----------------------
|
||||
|
||||
Applets don't render unless something on the display needs to change.
|
||||
An applet is expected to determine for itself when it has new info to display.
|
||||
It should interact with the firmware via the MeshModule API, via Observables, etc.
|
||||
Please don't directly add hooks throughout the existing firmware code.
|
||||
|
||||
When an applet decides it would like to update the display, it should call requestUpdate()
|
||||
The WindowManager will shortly call the onRender() method for all affected applets
|
||||
|
||||
An Applet may be unexpectedly asked to render at any point in time.
|
||||
|
||||
Applets should cache their data, but not their pixel output: they should re-render when onRender runs.
|
||||
An Applet's dimensions are not know until onRender is called, so pre-rendering of UI elements is prohibited.
|
||||
|
||||
Tiles
|
||||
-----
|
||||
|
||||
Applets are assigned to "Tiles".
|
||||
Assigning an applet to a tile creates a reciprocal link between the two.
|
||||
When an applet renders, it passes pixels to its tile.
|
||||
The tile translates these to the correct position, to be placed into the fullscreen framebuffer.
|
||||
User applets don't get to choose their own tile; the multiplexing is handled by the WindowManager.
|
||||
System applets might do strange things though.
|
||||
|
||||
Foreground and Background
|
||||
-------------------------
|
||||
|
||||
The user can cycle between applets by short-pressing the user button.
|
||||
Any applets which are currently displayed on the display are "foreground".
|
||||
When the user button is short pressed, and an applet is hidden, it becomes "background".
|
||||
|
||||
Although the WindowManager will not render background applets, they should still collect data,
|
||||
so they are ready to display when they are brought to foreground again.
|
||||
Even if they are in background, Applets should still request updates when an event affects them,
|
||||
as the user may have given them permission to "autoshow"; bringing themselves foreground automatically
|
||||
|
||||
Applets can implement the onForeground and onBackground methods to handle this change in state.
|
||||
They can also check their state by calling isForeground() at any time.
|
||||
|
||||
Active and Inactive
|
||||
-------------------
|
||||
|
||||
The user can select which applets are available, using the onscreen applet selection menu.
|
||||
Applets which are enabled in this menu are "active"; otherwise they are "inactive".
|
||||
|
||||
An inactive applet is expected not collect data; not to consume resources.
|
||||
Applets are activated at boot, or when enabled via the menu.
|
||||
They are deactivated at shutdown, or when disabled via the menu.
|
||||
|
||||
Applets can implement the onActivation and onDeactivation methods to handle this change in state.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include <GFX.h>
|
||||
|
||||
#include "./AppletFont.h"
|
||||
#include "./Applets/System/Notification/Notification.h"
|
||||
#include "./Tile.h"
|
||||
#include "./Types.h"
|
||||
#include "./WindowManager.h"
|
||||
#include "graphics/niche/Drivers/EInk/EInk.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
using NicheGraphics::Drivers::EInk;
|
||||
using std::to_string;
|
||||
|
||||
class Tile;
|
||||
class WindowManager;
|
||||
|
||||
class Applet : public GFX
|
||||
{
|
||||
public:
|
||||
Applet();
|
||||
|
||||
void setTile(Tile *t); // Applets draw via a tile (for multiplexing)
|
||||
Tile *getTile();
|
||||
|
||||
void render();
|
||||
bool wantsToRender(); // Check whether applet wants to render
|
||||
bool wantsToAutoshow(); // Check whether applets wants to become foreground, to show new data, if permitted
|
||||
Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer
|
||||
void updateDimensions(); // Get current size from tile
|
||||
void resetDrawingSpace(); // Makes sure every render starts with same parameters
|
||||
|
||||
// Change the applet's state
|
||||
|
||||
void activate();
|
||||
void deactivate();
|
||||
void bringToForeground();
|
||||
void sendToBackground();
|
||||
|
||||
// Info about applet's state
|
||||
|
||||
bool isActive();
|
||||
bool isForeground();
|
||||
|
||||
// Allow derived applets to handle changes in state
|
||||
|
||||
virtual void onRender() = 0; // All drawing happens here
|
||||
virtual void onActivate() {}
|
||||
virtual void onDeactivate() {}
|
||||
virtual void onForeground() {}
|
||||
virtual void onBackground() {}
|
||||
virtual void onShutdown() {}
|
||||
virtual void onButtonShortPress() {} // For use by System Applets only
|
||||
virtual void onButtonLongPress() {} // For use by System Applets only
|
||||
virtual void onLockAvailable() {} // For use by System Applets only
|
||||
|
||||
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
|
||||
|
||||
static void setDefaultFonts(AppletFont large, AppletFont small); // Set the general purpose fonts
|
||||
static uint16_t getHeaderHeight(); // How tall is the "standard" applet header
|
||||
|
||||
const char *name = nullptr; // Shown in applet selection menu
|
||||
|
||||
protected:
|
||||
// Place a single pixel. All drawing methods output through here
|
||||
void drawPixel(int16_t x, int16_t y, uint16_t color) override;
|
||||
|
||||
// Tell WindowManager to update display
|
||||
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED);
|
||||
|
||||
// Ask for applet to be moved to foreground
|
||||
void requestAutoshow();
|
||||
|
||||
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
|
||||
void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region
|
||||
void resetCrop(); // Removes setCrop()
|
||||
|
||||
void setFont(AppletFont f);
|
||||
AppletFont getFont();
|
||||
|
||||
uint16_t getTextWidth(std::string text);
|
||||
uint16_t getTextWidth(const char *text);
|
||||
|
||||
void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||
void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||
void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY);
|
||||
|
||||
// Print text, with per-word line wrapping
|
||||
void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text);
|
||||
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text);
|
||||
|
||||
void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines
|
||||
void drawHeader(std::string text); // Draw the standard applet header
|
||||
|
||||
static constexpr float LOGO_ASPECT_RATIO = 1.9; // Width:Height for drawing the Meshtastic logo
|
||||
uint16_t getLogoWidth(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
|
||||
uint16_t getLogoHeight(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
|
||||
void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height); // Draw the meshtastic logo
|
||||
|
||||
std::string hexifyNodeNum(NodeNum num);
|
||||
SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value
|
||||
std::string getTimeString(uint32_t epochSeconds); // Human readable
|
||||
std::string getTimeString(); // Current time, human readable
|
||||
uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu
|
||||
std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric
|
||||
|
||||
static AppletFont fontSmall, fontLarge; // General purpose fonts, used cross-applet
|
||||
|
||||
private:
|
||||
Tile *assignedTile = nullptr; // Rendered pixels are fed into a Tile object, which translates them, then passes to WM
|
||||
bool active = false; // Has the user enabled this applet (at run-time)?
|
||||
bool foreground = false; // Is the applet currently drawn on a tile?
|
||||
|
||||
bool wantRender = false; // In some situations, checked by WindowManager when updating, to skip unneeded redrawing.
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
AppletFont currentFont; // As passed to setFont
|
||||
|
||||
// As set by setCrop
|
||||
int16_t cropLeft;
|
||||
int16_t cropTop;
|
||||
uint16_t cropWidth;
|
||||
uint16_t cropHeight;
|
||||
};
|
||||
|
||||
}; // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,208 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./AppletFont.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::AppletFont::AppletFont()
|
||||
{
|
||||
// Default constructor uses the in-built AdafruitGFX font
|
||||
}
|
||||
|
||||
InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont) : gfxFont(&adafruitGFXFont)
|
||||
{
|
||||
// AdafruitGFX fonts are drawn relative to a "cursor line";
|
||||
// they print as if the glyphs resting on the line of piece of ruled paper.
|
||||
// The glyphs also each have a different height.
|
||||
|
||||
// To simplify drawing, we will scan the entire font now, and determine an appropriate height for a line of text
|
||||
// We also need to know where that "cursor line" sits inside this "line height";
|
||||
// we need this additional info in order to align text by top-left, bottom-right, etc
|
||||
|
||||
// AdafruitGFX fonts do declare a line-height, but this seems to include a certain amount of padding,
|
||||
// which we'd rather not deal with. If we want padding, we'll add it manually.
|
||||
|
||||
// Scan each glyph in the AdafruitGFX font
|
||||
for (uint16_t i = 0; i <= (gfxFont->last - gfxFont->first); i++) {
|
||||
uint8_t glyphHeight = gfxFont->glyph[i].height; // Height of glyph
|
||||
this->height = max(this->height, glyphHeight); // Store if it's a new max
|
||||
|
||||
// Calculate how far the glyph rises the cursor line
|
||||
// Store if new max value
|
||||
// Caution: signed and unsigned types
|
||||
int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset;
|
||||
if (glyphAscender > 0)
|
||||
this->ascenderHeight = max(this->ascenderHeight, (uint8_t)glyphAscender);
|
||||
}
|
||||
|
||||
// Determine how far characters may hang "below the line"
|
||||
descenderHeight = height - ascenderHeight;
|
||||
|
||||
// Find how far the cursor advances when we "print" a space character
|
||||
spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance;
|
||||
}
|
||||
|
||||
uint8_t InkHUD::AppletFont::lineHeight()
|
||||
{
|
||||
return this->height;
|
||||
}
|
||||
|
||||
// AdafruitGFX fonts print characters so that they nicely on an imaginary line (think: ruled paper).
|
||||
// This value is the height of the font, above that imaginary line.
|
||||
// Used to calculate the true height of the font
|
||||
uint8_t InkHUD::AppletFont::heightAboveCursor()
|
||||
{
|
||||
return this->ascenderHeight;
|
||||
}
|
||||
|
||||
// AdafruitGFX fonts print characters so that they nicely on an imaginary line (think: ruled paper).
|
||||
// This value is the height of the font, below that imaginary line.
|
||||
// Used to calculate the true height of the font
|
||||
uint8_t InkHUD::AppletFont::heightBelowCursor()
|
||||
{
|
||||
return this->descenderHeight;
|
||||
}
|
||||
|
||||
// Width of the space character
|
||||
// Used with Applet::printWrapped
|
||||
uint8_t InkHUD::AppletFont::widthBetweenWords()
|
||||
{
|
||||
return this->spaceCharWidth;
|
||||
}
|
||||
|
||||
// Add to the list of substituted glyphs
|
||||
// This "find and replace" operation will be run before text is printed
|
||||
// Used to swap out UTF8 special characters, either with a custom font, or with a suitable ASCII approximation
|
||||
void InkHUD::AppletFont::addSubstitution(const char *from, const char *to)
|
||||
{
|
||||
substitutions.push_back({.from = from, .to = to});
|
||||
}
|
||||
|
||||
// Run all registered subtitutions on a string
|
||||
// Used to swap out UTF8 special chars
|
||||
void InkHUD::AppletFont::applySubstitutions(std::string *text)
|
||||
{
|
||||
// For each substitution
|
||||
for (Substitution s : substitutions) {
|
||||
|
||||
// Find and replace
|
||||
// - search for Substitution::from
|
||||
// - replace with Subsitution::to
|
||||
size_t i = text->find(s.from);
|
||||
while (i != std::string::npos) {
|
||||
text->replace(i, strlen(s.from), s.to);
|
||||
i = text->find(s.from, i); // Continue looking from last position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply a set of substitutions which remap UTF8 for a Windows-1251 font
|
||||
// Windows-1251 is an 8-bit character encoding, designed to cover languages that use the Cyrillic script
|
||||
void InkHUD::AppletFont::addSubstitutionsWin1251()
|
||||
{
|
||||
addSubstitution("Ђ", "\x80");
|
||||
addSubstitution("Ѓ", "\x81");
|
||||
addSubstitution("ѓ", "\x83");
|
||||
addSubstitution("€", "\x88");
|
||||
addSubstitution("Љ", "\x8A");
|
||||
addSubstitution("Њ", "\x8C");
|
||||
addSubstitution("Ќ", "\x8D");
|
||||
addSubstitution("Ћ", "\x8E");
|
||||
addSubstitution("Џ", "\x8F");
|
||||
|
||||
addSubstitution("ђ", "\x90");
|
||||
addSubstitution("љ", "\x9A");
|
||||
addSubstitution("њ", "\x9C");
|
||||
addSubstitution("ќ", "\x9D");
|
||||
addSubstitution("ћ", "\x9E");
|
||||
addSubstitution("џ", "\x9F");
|
||||
|
||||
addSubstitution("Ў", "\xA1");
|
||||
addSubstitution("ў", "\xA2");
|
||||
addSubstitution("Ј", "\xA3");
|
||||
addSubstitution("Ґ", "\xA5");
|
||||
addSubstitution("Ё", "\xA8");
|
||||
addSubstitution("Є", "\xAA");
|
||||
addSubstitution("Ї", "\xAF");
|
||||
|
||||
addSubstitution("І", "\xB2");
|
||||
addSubstitution("і", "\xB3");
|
||||
addSubstitution("ґ", "\xB4");
|
||||
addSubstitution("ё", "\xB8");
|
||||
addSubstitution("№", "\xB9");
|
||||
addSubstitution("є", "\xBA");
|
||||
addSubstitution("ј", "\xBC");
|
||||
addSubstitution("Ѕ", "\xBD");
|
||||
addSubstitution("ѕ", "\xBE");
|
||||
addSubstitution("ї", "\xBF");
|
||||
|
||||
addSubstitution("А", "\xC0");
|
||||
addSubstitution("Б", "\xC1");
|
||||
addSubstitution("В", "\xC2");
|
||||
addSubstitution("Г", "\xC3");
|
||||
addSubstitution("Д", "\xC4");
|
||||
addSubstitution("Е", "\xC5");
|
||||
addSubstitution("Ж", "\xC6");
|
||||
addSubstitution("З", "\xC7");
|
||||
addSubstitution("И", "\xC8");
|
||||
addSubstitution("Й", "\xC9");
|
||||
addSubstitution("К", "\xCA");
|
||||
addSubstitution("Л", "\xCB");
|
||||
addSubstitution("М", "\xCC");
|
||||
addSubstitution("Н", "\xCD");
|
||||
addSubstitution("О", "\xCE");
|
||||
addSubstitution("П", "\xCF");
|
||||
|
||||
addSubstitution("Р", "\xD0");
|
||||
addSubstitution("С", "\xD1");
|
||||
addSubstitution("Т", "\xD2");
|
||||
addSubstitution("У", "\xD3");
|
||||
addSubstitution("Ф", "\xD4");
|
||||
addSubstitution("Х", "\xD5");
|
||||
addSubstitution("Ц", "\xD6");
|
||||
addSubstitution("Ч", "\xD7");
|
||||
addSubstitution("Ш", "\xD8");
|
||||
addSubstitution("Щ", "\xD9");
|
||||
addSubstitution("Ъ", "\xDA");
|
||||
addSubstitution("Ы", "\xDB");
|
||||
addSubstitution("Ь", "\xDC");
|
||||
addSubstitution("Э", "\xDD");
|
||||
addSubstitution("Ю", "\xDE");
|
||||
addSubstitution("Я", "\xDF");
|
||||
|
||||
addSubstitution("а", "\xE0");
|
||||
addSubstitution("б", "\xE1");
|
||||
addSubstitution("в", "\xE2");
|
||||
addSubstitution("г", "\xE3");
|
||||
addSubstitution("д", "\xE4");
|
||||
addSubstitution("е", "\xE5");
|
||||
addSubstitution("ж", "\xE6");
|
||||
addSubstitution("з", "\xE7");
|
||||
addSubstitution("и", "\xE8");
|
||||
addSubstitution("й", "\xE9");
|
||||
addSubstitution("к", "\xEA");
|
||||
addSubstitution("л", "\xEB");
|
||||
addSubstitution("м", "\xEC");
|
||||
addSubstitution("н", "\xED");
|
||||
addSubstitution("о", "\xEE");
|
||||
addSubstitution("п", "\xEF");
|
||||
|
||||
addSubstitution("р", "\xF0");
|
||||
addSubstitution("с", "\xF1");
|
||||
addSubstitution("т", "\xF2");
|
||||
addSubstitution("у", "\xF3");
|
||||
addSubstitution("ф", "\xF4");
|
||||
addSubstitution("х", "\xF5");
|
||||
addSubstitution("ц", "\xF6");
|
||||
addSubstitution("ч", "\xF7");
|
||||
addSubstitution("ш", "\xF8");
|
||||
addSubstitution("щ", "\xF9");
|
||||
addSubstitution("ъ", "\xFA");
|
||||
addSubstitution("ы", "\xFB");
|
||||
addSubstitution("ь", "\xFC");
|
||||
addSubstitution("э", "\xFD");
|
||||
addSubstitution("ю", "\xFE");
|
||||
addSubstitution("я", "\xFF");
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,59 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Wrapper class for an AdafruitGFX font
|
||||
Pre-calculates some font dimension info which InkHUD uses repeatedly
|
||||
|
||||
Also contains an optional set of "substitutions".
|
||||
These can be used to detect special UTF8 chars, and replace occurrences with a remapped char val to suit a custom font
|
||||
These can also be used to swap UTF8 chars for a suitable ASCII substitution (e.g. German ö -> oe, etc)
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include <GFX.h>
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
// An AdafruitGFX font, bundled with precalculated dimensions which are used frequently by InkHUD
|
||||
class AppletFont
|
||||
{
|
||||
public:
|
||||
AppletFont();
|
||||
AppletFont(const GFXfont &adafruitGFXFont);
|
||||
uint8_t lineHeight();
|
||||
uint8_t heightAboveCursor();
|
||||
uint8_t heightBelowCursor();
|
||||
uint8_t widthBetweenWords();
|
||||
|
||||
void applySubstitutions(std::string *text); // Run all char-substitution operations, prior to printing
|
||||
void addSubstitution(const char *from, const char *to); // Register a find-replace action, for remapping UTF8 chars
|
||||
void addSubstitutionsWin1251(); // Cyrillic fonts: remap UTF8 values to their Win-1251 equivalent
|
||||
// Todo: Polish font
|
||||
|
||||
const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font
|
||||
|
||||
private:
|
||||
uint8_t height = 8; // Default value: in-built AdafruitGFX font
|
||||
uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font
|
||||
uint8_t descenderHeight = 8; // Default value: in-built AdafruitGFX font
|
||||
uint8_t spaceCharWidth = 8; // Default value: in-built AdafruitGFX font
|
||||
|
||||
// One pair of find-replace values, for substituting or remapping UTF8 chars
|
||||
struct Substitution {
|
||||
const char *from;
|
||||
const char *to;
|
||||
};
|
||||
|
||||
// List of all character substitutions to run, prior to printing a string
|
||||
std::vector<Substitution> substitutions;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,429 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./MapApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::MapApplet::onRender()
|
||||
{
|
||||
setFont(fontSmall);
|
||||
|
||||
// Abort if no markers to render
|
||||
if (!enoughMarkers()) {
|
||||
printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Node positions", CENTER, MIDDLE);
|
||||
printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find center of map
|
||||
// - latitude and longitude
|
||||
// - will be placed at X(0.5), Y(0.5)
|
||||
getMapCenter(&latCenter, &lngCenter);
|
||||
|
||||
// Calculate North+East distance of each node to map center
|
||||
// - which nodes to use controlled by virtual shouldDrawNode method
|
||||
calculateAllMarkers();
|
||||
|
||||
// Set the region shown on the map
|
||||
// - default: fit all nodes, plus padding
|
||||
// - maybe overriden by derived applet
|
||||
getMapSize(&widthMeters, &heightMeters);
|
||||
|
||||
// Set the metersToPx conversion value
|
||||
calculateMapScale();
|
||||
|
||||
// Special marker for own node
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (ourNode && nodeDB->hasValidPosition(ourNode))
|
||||
drawLabeledMarker(ourNode);
|
||||
|
||||
// Draw all markers
|
||||
for (Marker m : markers) {
|
||||
int16_t x = X(0.5) + (m.eastMeters * metersToPx);
|
||||
int16_t y = Y(0.5) - (m.northMeters * metersToPx);
|
||||
|
||||
// Cross Size
|
||||
constexpr uint16_t csMin = 5;
|
||||
constexpr uint16_t csMax = 12;
|
||||
|
||||
// Too many hops away
|
||||
if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) // Too many mops
|
||||
printAt(x, y, "!", CENTER, MIDDLE);
|
||||
else if (!m.hasHopsAway) // Unknown hops
|
||||
drawCross(x, y, csMin);
|
||||
else // The fewer hops, the larger the cross
|
||||
drawCross(x, y, map(m.hopsAway, 0, config.lora.hop_limit, csMax, csMin));
|
||||
}
|
||||
}
|
||||
|
||||
// Find the center point, in the middle of all node positions
|
||||
// Calculated values are written to the *lat and *long pointer args
|
||||
// - Finds the "mean lat long"
|
||||
// - Calculates furthest nodes from "mean lat long"
|
||||
// - Place map center directly between these furthest nodes
|
||||
|
||||
void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
|
||||
{
|
||||
// Find mean lat long coords
|
||||
// ============================
|
||||
// - assigning X, Y and Z values to position on Earth's surface in 3D space, relative to center of planet
|
||||
// - averages the x, y and z coords
|
||||
// - uses tan to find angles for lat / long degrees
|
||||
// - longitude: triangle formed by x and y (on plane of the equator)
|
||||
// - latitude: triangle formed by z (north south),
|
||||
// and the line along plane of equator which stetches from earth's axis to where point xyz intersects planet's surface
|
||||
|
||||
// Working totals, averaged after nodeDB processed
|
||||
uint32_t positionCount = 0;
|
||||
float xAvg = 0;
|
||||
float yAvg = 0;
|
||||
float zAvg = 0;
|
||||
|
||||
// For each node in db
|
||||
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
// Skip if no position
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
continue;
|
||||
|
||||
// Skip if derived applet doesn't want to show this node on the map
|
||||
if (!shouldDrawNode(node))
|
||||
continue;
|
||||
|
||||
// Latitude and Longitude of node, in radians
|
||||
float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD;
|
||||
float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD;
|
||||
|
||||
// Convert to cartesian points, with center of earth at 0, 0, 0
|
||||
// Exact distance from center is irrelevant, as we're only interested in the vector
|
||||
float x = cos(latRad) * cos(lngRad);
|
||||
float y = cos(latRad) * sin(lngRad);
|
||||
float z = sin(latRad);
|
||||
|
||||
// To find mean values shortly
|
||||
xAvg += x;
|
||||
yAvg += y;
|
||||
zAvg += z;
|
||||
positionCount++;
|
||||
}
|
||||
|
||||
// All NodeDB processed, find mean values
|
||||
xAvg /= positionCount;
|
||||
yAvg /= positionCount;
|
||||
zAvg /= positionCount;
|
||||
|
||||
// Longitude from cartesian coords
|
||||
// (Angle from 3D coords describing a point of globe's surface)
|
||||
/*
|
||||
UK
|
||||
/-------\
|
||||
(Top View) /- -\
|
||||
/- (You) -\
|
||||
/- . -\
|
||||
/- . X -\
|
||||
Asia - ... - USA
|
||||
\- Y -/
|
||||
\- -/
|
||||
\- -/
|
||||
\- -/
|
||||
\- -----/
|
||||
Pacific
|
||||
|
||||
*/
|
||||
|
||||
*lng = atan2(yAvg, xAvg) * RAD_TO_DEG;
|
||||
|
||||
// Latitude from cartesian cooods
|
||||
// (Angle from 3D coords describing a point on the globe's surface)
|
||||
// As latitude increases, distance from the Earth's north-south axis out to our surface point decreases.
|
||||
// Means we need to first find the hypotenuse which becomes base of our triangle in the second step
|
||||
/*
|
||||
UK North
|
||||
/-------\ (Front View) /-------\
|
||||
(Top View) /- -\ /- -\
|
||||
/- (You) -\ /-(You) -\
|
||||
/- /. -\ /- . -\
|
||||
/- √X²+Y²/ . X -\ /- Z . -\
|
||||
Asia - /... - USA - ..... -
|
||||
\- Y -/ \- √X²+Y² -/
|
||||
\- -/ \- -/
|
||||
\- -/ \- -/
|
||||
\- -/ \- -/
|
||||
\- -----/ \- -----/
|
||||
Pacific South
|
||||
*/
|
||||
|
||||
float hypotenuse = sqrt((xAvg * xAvg) + (yAvg * yAvg)); // Distance from globe's north-south axis to surface intersect
|
||||
*lat = atan2(zAvg, hypotenuse) * RAD_TO_DEG;
|
||||
|
||||
// ----------------------------------------------
|
||||
// This has given us the "mean position"
|
||||
// This will be a position *somewhere* near the center of our nodes.
|
||||
// What we actually want is to place our center so that our outermost nodes end up on the border of our map.
|
||||
// The only real use of our "mean position" is to give us a reference frame:
|
||||
// which direction is east, and which is west.
|
||||
//------------------------------------------------
|
||||
|
||||
// Find furthest nodes from "mean lat long"
|
||||
// ========================================
|
||||
|
||||
float northernmost = latCenter;
|
||||
float southernmost = latCenter;
|
||||
float easternmost = lngCenter;
|
||||
float westernmost = lngCenter;
|
||||
|
||||
for (uint8_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
// Skip if no position
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
continue;
|
||||
|
||||
// Skip if derived applet doesn't want to show this node on the map
|
||||
if (!shouldDrawNode(node))
|
||||
continue;
|
||||
|
||||
// Check for a new top or bottom latitude
|
||||
float lat = node->position.latitude_i * 1e-7;
|
||||
northernmost = max(northernmost, lat);
|
||||
southernmost = min(southernmost, lat);
|
||||
|
||||
// Longitude is trickier
|
||||
float lng = node->position.longitude_i * 1e-7;
|
||||
float degEastward = fmod(((lng - lngCenter) + 360), 360); // Degrees travelled east from lngCenter to reach node
|
||||
float degWestward = abs(fmod(((lng - lngCenter) - 360), 360)); // Degrees travelled west from lngCenter to reach node
|
||||
if (degEastward < degWestward)
|
||||
easternmost = max(easternmost, lngCenter + degEastward);
|
||||
else
|
||||
westernmost = min(westernmost, lngCenter - degWestward);
|
||||
}
|
||||
|
||||
// Todo: check for issues with map spans >180 deg. MQTT only..
|
||||
latCenter = (northernmost + southernmost) / 2;
|
||||
lngCenter = (westernmost + easternmost) / 2;
|
||||
|
||||
// In case our new center is west of -180, or east of +180, for some reason
|
||||
lngCenter = fmod(lngCenter, 180);
|
||||
}
|
||||
|
||||
// Size of map in meters
|
||||
// Grown to fit the nodes furthest from map center
|
||||
// Overridable if derived applet wants a custom map size (fixed size?)
|
||||
void InkHUD::MapApplet::getMapSize(uint32_t *widthMeters, uint32_t *heightMeters)
|
||||
{
|
||||
// Reset the value
|
||||
*widthMeters = 0;
|
||||
*heightMeters = 0;
|
||||
|
||||
// Find the greatest distance horizontally and vertically from map center
|
||||
for (Marker m : markers) {
|
||||
*widthMeters = max(*widthMeters, (uint32_t)abs(m.eastMeters) * 2);
|
||||
*heightMeters = max(*heightMeters, (uint32_t)abs(m.northMeters) * 2);
|
||||
}
|
||||
|
||||
// Add padding
|
||||
*widthMeters *= 1.1;
|
||||
*heightMeters *= 1.1;
|
||||
}
|
||||
|
||||
// Convert and store info we need for drawing a marker
|
||||
// Lat / long to "meters relative to map center", for position on screen
|
||||
// Info about hopsAway, for marker size
|
||||
InkHUD::MapApplet::Marker InkHUD::MapApplet::calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway)
|
||||
{
|
||||
assert(lat != 0 || lng != 0); // Not null island. Applets should check this before calling.
|
||||
|
||||
// Bearing and distance from map center to node
|
||||
float distanceFromCenter = GeoCoord::latLongToMeter(latCenter, lngCenter, lat, lng);
|
||||
float bearingFromCenter = GeoCoord::bearing(latCenter, lngCenter, lat, lng); // in radians
|
||||
|
||||
// Split into meters north and meters east components (signed)
|
||||
// - signedness of cos / sin automatically sets negative if south or west
|
||||
float northMeters = cos(bearingFromCenter) * distanceFromCenter;
|
||||
float eastMeters = sin(bearingFromCenter) * distanceFromCenter;
|
||||
|
||||
// Store this as a new marker
|
||||
Marker m;
|
||||
m.eastMeters = eastMeters;
|
||||
m.northMeters = northMeters;
|
||||
m.hasHopsAway = hasHopsAway;
|
||||
m.hopsAway = hopsAway;
|
||||
return m;
|
||||
}
|
||||
|
||||
// Draw a marker on the map for a node, with a shortname label, and backing box
|
||||
void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
|
||||
{
|
||||
// Find x and y position based on node's position in nodeDB
|
||||
assert(nodeDB->hasValidPosition(node));
|
||||
Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
||||
node->position.longitude_i * 1e-7, // Long, convered from Meshtastic's internal int32 style
|
||||
node->has_hops_away, // Is the hopsAway number valid
|
||||
node->hops_away // Hops away
|
||||
);
|
||||
|
||||
// Convert to pixel coords
|
||||
int16_t markerX = X(0.5) + (m.eastMeters * metersToPx);
|
||||
int16_t markerY = Y(0.5) - (m.northMeters * metersToPx);
|
||||
|
||||
constexpr uint16_t paddingH = 2;
|
||||
constexpr uint16_t paddingW = 4;
|
||||
uint16_t paddingInnerW = 2; // Zero'd out if no text
|
||||
constexpr uint16_t markerSizeMax = 12; // Size of cross (if marker uses a cross)
|
||||
constexpr uint16_t markerSizeMin = 5;
|
||||
|
||||
int16_t textX;
|
||||
int16_t textY;
|
||||
uint16_t textW;
|
||||
uint16_t textH;
|
||||
int16_t labelX;
|
||||
int16_t labelY;
|
||||
uint16_t labelW;
|
||||
uint16_t labelH;
|
||||
uint8_t markerSize;
|
||||
|
||||
bool tooManyHops = node->hops_away > config.lora.hop_limit;
|
||||
bool isOurNode = node->num == nodeDB->getNodeNum();
|
||||
bool unknownHops = !node->has_hops_away && !isOurNode;
|
||||
|
||||
// We will draw a left or right hand variant, to place text towards screen center
|
||||
// Hopfully avoid text spilling off screen
|
||||
// Most values are the same, regardless of left-right handedness
|
||||
|
||||
// Pick emblem style
|
||||
if (tooManyHops)
|
||||
markerSize = getTextWidth("!");
|
||||
else if (unknownHops)
|
||||
markerSize = markerSizeMin;
|
||||
else
|
||||
markerSize = map(node->hops_away, 0, config.lora.hop_limit, markerSizeMax, markerSizeMin);
|
||||
|
||||
// Common dimensions (left or right variant)
|
||||
textW = getTextWidth(node->user.short_name);
|
||||
if (textW == 0)
|
||||
paddingInnerW = 0; // If no text, no padding for text
|
||||
textH = fontSmall.lineHeight();
|
||||
labelH = paddingH + max((int16_t)(textH), (int16_t)markerSize) + paddingH;
|
||||
labelY = markerY - (labelH / 2);
|
||||
textY = markerY;
|
||||
labelW = paddingW + markerSize + paddingInnerW + textW + paddingW; // Width is same whether right or left hand variant
|
||||
|
||||
// Left-side variant
|
||||
if (markerX < width() / 2) {
|
||||
labelX = markerX - (markerSize / 2) - paddingW;
|
||||
textX = labelX + paddingW + markerSize + paddingInnerW;
|
||||
}
|
||||
|
||||
// Right-side variant
|
||||
else {
|
||||
labelX = markerX - (markerSize / 2) - paddingInnerW - textW - paddingW;
|
||||
textX = labelX + paddingW;
|
||||
}
|
||||
|
||||
// Backing box
|
||||
fillRect(labelX, labelY, labelW, labelH, WHITE);
|
||||
drawRect(labelX, labelY, labelW, labelH, BLACK);
|
||||
|
||||
// Short name
|
||||
printAt(textX, textY, node->user.short_name, LEFT, MIDDLE);
|
||||
|
||||
// If the label is for our own node,
|
||||
// fade it by overdrawing partially with white
|
||||
if (node == nodeDB->getMeshNode(nodeDB->getNodeNum()))
|
||||
hatchRegion(labelX, labelY, labelW, labelH, 2, WHITE);
|
||||
|
||||
// Draw the marker emblem
|
||||
// - after the fading, because hatching (own node) can align with cross and make it look weird
|
||||
if (tooManyHops)
|
||||
printAt(markerX, markerY, "!", CENTER, MIDDLE);
|
||||
else
|
||||
drawCross(markerX, markerY, markerSize); // The fewer the hops, the larger the marker. Also handles unknownHops
|
||||
}
|
||||
|
||||
// Check if we actually have enough nodes which would be shown on the map
|
||||
// Need at least two, to draw a sensible map
|
||||
bool InkHUD::MapApplet::enoughMarkers()
|
||||
{
|
||||
uint8_t count = 0;
|
||||
for (uint8_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
// Count nodes
|
||||
if (nodeDB->hasValidPosition(node) && shouldDrawNode(node))
|
||||
count++;
|
||||
|
||||
// We need to find two
|
||||
if (count == 2)
|
||||
return true; // Two nodes is enough for a sensible map
|
||||
}
|
||||
|
||||
return false; // No nodes would be drawn (or just the one, uselessly at 0,0)
|
||||
}
|
||||
|
||||
// Calculate how far north and east of map center each node is
|
||||
// Derived applets can control which nodes to calculate (and later, draw) by overriding MapApplet::shouldDrawNode
|
||||
void InkHUD::MapApplet::calculateAllMarkers()
|
||||
{
|
||||
// Clear old markers
|
||||
markers.clear();
|
||||
|
||||
// For each node in db
|
||||
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
// Skip if no position
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
continue;
|
||||
|
||||
// Skip if derived applet doesn't want to show this node on the map
|
||||
if (!shouldDrawNode(node))
|
||||
continue;
|
||||
|
||||
// Skip if our own node
|
||||
// - special handling in render()
|
||||
if (node->num == nodeDB->getNodeNum())
|
||||
continue;
|
||||
|
||||
// Calculate marker and store it
|
||||
markers.push_back(
|
||||
calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
||||
node->position.longitude_i * 1e-7, // Long, convered from Meshtastic's internal int32 style
|
||||
node->has_hops_away, // Is the hopsAway number valid
|
||||
node->hops_away // Hops away
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the conversion factor between metres, and pixels on screen
|
||||
// May be overriden by derived applet, if custom scale required (fixed map size?)
|
||||
void InkHUD::MapApplet::calculateMapScale()
|
||||
{
|
||||
// Aspect ratio of map and screen
|
||||
// - larger = wide, smaller = tall
|
||||
// - used to set scale, so that widest map dimension fits in applet
|
||||
float mapAspectRatio = (float)widthMeters / heightMeters;
|
||||
float appletAspectRatio = (float)width() / height();
|
||||
|
||||
// "Shrink to fit"
|
||||
// Scale the map so that the largest dimension is fully displayed
|
||||
// Because aspect ratio will be maintained, the other dimension will appear "padded"
|
||||
if (mapAspectRatio > appletAspectRatio)
|
||||
metersToPx = (float)width() / widthMeters; // Too wide for applet. Constrain to fit width.
|
||||
else
|
||||
metersToPx = (float)height() / heightMeters; // Too tall for applet. Constrain to fit height.
|
||||
}
|
||||
|
||||
// Draw an x, centered on a specific point
|
||||
// Most markers will draw with this method
|
||||
void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size)
|
||||
{
|
||||
int16_t x0 = x - (size / 2);
|
||||
int16_t y0 = y - (size / 2);
|
||||
int16_t x1 = x0 + size - 1;
|
||||
int16_t y1 = y0 + size - 1;
|
||||
drawLine(x0, y0, x1, y1, BLACK);
|
||||
drawLine(x0, y1, x1, y0, BLACK);
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,66 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Base class for Applets which show nodes on a map
|
||||
|
||||
Plots position of for a selection of nodes, with north facing up.
|
||||
Size of cross represents hops away.
|
||||
Our own node is identified with a faded label.
|
||||
|
||||
The base applet doesn't handle any events; this is left to the derived applets.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
#include "MeshModule.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class MapApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
|
||||
protected:
|
||||
virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes
|
||||
virtual void getMapCenter(float *lat, float *lng);
|
||||
virtual void getMapSize(uint32_t *widthMeters, uint32_t *heightMeters);
|
||||
|
||||
bool enoughMarkers(); // Anything to draw?
|
||||
void drawLabeledMarker(meshtastic_NodeInfoLite *node); // Highlight a specific marker
|
||||
|
||||
private:
|
||||
// Position of markers to be drawn, relative to map center
|
||||
// HopsAway info used to determine marker size
|
||||
struct Marker {
|
||||
float eastMeters = 0; // Meters east of mapCenter. Negative if west.
|
||||
float northMeters = 0; // Meters north of mapCenter. Negative if south.
|
||||
bool hasHopsAway = false;
|
||||
uint8_t hopsAway = 0;
|
||||
};
|
||||
|
||||
Marker calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway);
|
||||
void calculateAllMarkers();
|
||||
void calculateMapScale(); // Conversion factor for meters to pixels
|
||||
void drawCross(int16_t x, int16_t y, uint8_t size); // Draw the X used for most markers
|
||||
|
||||
float metersToPx = 0; // Conversion factor for meters to pixels
|
||||
float latCenter = 0; // Map center: latitude
|
||||
float lngCenter = 0; // Map center: longitude
|
||||
|
||||
std::list<Marker> markers;
|
||||
uint32_t widthMeters = 0; // Map width: meters
|
||||
uint32_t heightMeters = 0; // Map height: meters
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,283 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "RTC.h"
|
||||
|
||||
#include "GeoCoord.h"
|
||||
#include "NodeDB.h"
|
||||
|
||||
#include "./NodeListApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::NodeListApplet::NodeListApplet(const char *name) : MeshModule(name)
|
||||
{
|
||||
// We only need to be promiscuous in order to hear NodeInfo, apparently. See NodeInfoModule
|
||||
// For all other packets, we manually reimplement isPromiscuous=false in wantPacket
|
||||
MeshModule::isPromiscuous = true;
|
||||
}
|
||||
|
||||
// Do we want to process this packet with handleReceived()?
|
||||
bool InkHUD::NodeListApplet::wantPacket(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
// Only interested if:
|
||||
return isActive() // Applet is active
|
||||
&& !isFromUs(p) // Packet is incoming (not outgoing)
|
||||
&& (isToUs(p) || isBroadcast(p->to) || // Either: intended for us,
|
||||
p->decoded.portnum == meshtastic_PortNum_NODEINFO_APP); // or nodeinfo
|
||||
|
||||
// Note: special handling of NodeInfo is to match NodeInfoModule
|
||||
// To match the behavior seen in the client apps:
|
||||
// - NodeInfoModule's ProtoBufModule base is "promiscuous"
|
||||
// - All other activity is *not* promiscuous
|
||||
// To achieve this, our MeshModule *is* promiscious, and we're manually reimplementing non-promiscuous behavior here,
|
||||
// to match the code in MeshModule::callModules
|
||||
}
|
||||
|
||||
// MeshModule packets arrive here
|
||||
// Extract the info and pass it to the derived applet
|
||||
// Derived applet will store the CardInfo and perform any required sorting of the CardInfo collection
|
||||
// Derived applet might also need to keep other tallies (active nodes count?)
|
||||
ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
// Abort if applet fully deactivated
|
||||
// Already handled by wantPacket in this case, but good practice for all applets, as some *do* require this early return
|
||||
if (!isActive())
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Assemble info: from this event
|
||||
CardInfo c;
|
||||
c.nodeNum = mp.from;
|
||||
c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi);
|
||||
|
||||
// Assemble info: from nodeDB (needed to detect changes)
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum);
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (node) {
|
||||
if (node->has_hops_away)
|
||||
c.hopsAway = node->hops_away;
|
||||
|
||||
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
|
||||
// Get lat and long as float
|
||||
// Meshtastic stores these as integers internally
|
||||
float ourLat = ourNode->position.latitude_i * 1e-7;
|
||||
float ourLong = ourNode->position.longitude_i * 1e-7;
|
||||
float theirLat = node->position.latitude_i * 1e-7;
|
||||
float theirLong = node->position.longitude_i * 1e-7;
|
||||
|
||||
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass to the derived applet
|
||||
// Derived applet is responsible for requesting update, if justified
|
||||
// That request will eventually trigger our class' onRender method
|
||||
handleParsed(c);
|
||||
|
||||
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
// Maximum number of cards we may ever need to render, in our tallest layout config
|
||||
// May be slightly in excess of the true value: header not accounted for
|
||||
uint8_t InkHUD::NodeListApplet::maxCards()
|
||||
{
|
||||
// Cache result. Shouldn't change during execution
|
||||
static uint8_t cards = 0;
|
||||
|
||||
if (!cards) {
|
||||
const uint16_t height = Tile::maxDisplayDimension();
|
||||
|
||||
// Use a loop instead of arithmetic, because it's easier for my brain to follow
|
||||
// Add cards one by one, until the latest card (without margin) extends below screen
|
||||
|
||||
uint16_t y = cardH; // First card: no margin above
|
||||
cards = 1;
|
||||
|
||||
while (y < height) {
|
||||
y += cardMarginH;
|
||||
y += cardH;
|
||||
cards++;
|
||||
}
|
||||
}
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
// Draw using info which derived applet placed into NodeListApplet::cards for us
|
||||
void InkHUD::NodeListApplet::onRender()
|
||||
{
|
||||
|
||||
// ================================
|
||||
// Draw the standard applet header
|
||||
// ================================
|
||||
|
||||
drawHeader(getHeaderText()); // Ask derived applet for the title
|
||||
|
||||
// Dimensions of the header
|
||||
int16_t headerDivY = getHeaderHeight() - 1;
|
||||
constexpr uint16_t padDivH = 2;
|
||||
|
||||
// ========================
|
||||
// Draw the main node list
|
||||
// ========================
|
||||
|
||||
// const uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards
|
||||
// const uint16_t cardH = fontLarge.lineHeight() + fontSmall.lineHeight() + cardMarginH;
|
||||
|
||||
// Imaginary vertical line dividing left-side and right-side info
|
||||
// Long-name will crop here
|
||||
const uint16_t dividerX = (width() - 1) - getTextWidth("X Hops");
|
||||
|
||||
// Y value (top) of the current card. Increases as we draw.
|
||||
uint16_t cardTopY = headerDivY + padDivH;
|
||||
|
||||
// -- Each node in list --
|
||||
for (auto card = cards.begin(); card != cards.end(); ++card) {
|
||||
|
||||
// Gather info
|
||||
// ========================================
|
||||
NodeNum &nodeNum = card->nodeNum;
|
||||
SignalStrength &signal = card->signal;
|
||||
std::string longName; // handled below
|
||||
std::string shortName; // handled below
|
||||
std::string distance; // handled below;
|
||||
uint8_t &hopsAway = card->hopsAway;
|
||||
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
|
||||
|
||||
// -- Shortname --
|
||||
// use "?" if unknown
|
||||
if (node && node->has_user)
|
||||
shortName = node->user.short_name;
|
||||
else
|
||||
shortName = "?";
|
||||
|
||||
// -- Longname --
|
||||
// use node id if unknown
|
||||
if (node && node->has_user)
|
||||
longName = node->user.long_name; // Found in nodeDB
|
||||
else {
|
||||
// Not found in nodeDB, show a hex nodeid instead
|
||||
longName = hexifyNodeNum(nodeNum);
|
||||
}
|
||||
|
||||
// -- Distance --
|
||||
if (card->distanceMeters != CardInfo::DISTANCE_UNKNOWN)
|
||||
distance = localizeDistance(card->distanceMeters);
|
||||
|
||||
// Draw the info
|
||||
// ====================================
|
||||
|
||||
// Define two lines of text for the card
|
||||
// We will center our text on these lines
|
||||
uint16_t lineAY = cardTopY + (fontLarge.lineHeight() / 2);
|
||||
uint16_t lineBY = cardTopY + fontLarge.lineHeight() + (fontSmall.lineHeight() / 2);
|
||||
|
||||
// Print the short name
|
||||
setFont(fontLarge);
|
||||
printAt(0, lineAY, shortName, LEFT, MIDDLE);
|
||||
|
||||
// Print the distance
|
||||
setFont(fontSmall);
|
||||
printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE);
|
||||
|
||||
// If we have a direct connection to the node, draw the signal indicator
|
||||
if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) {
|
||||
uint16_t signalW = getTextWidth("Xkm"); // Indicator should be similar width to distance label
|
||||
uint16_t signalH = fontLarge.lineHeight() * 0.75;
|
||||
int16_t signalY = lineAY + (fontLarge.lineHeight() / 2) - (fontLarge.lineHeight() * 0.75);
|
||||
int16_t signalX = width() - signalW;
|
||||
drawSignalIndicator(signalX, signalY, signalW, signalH, signal);
|
||||
}
|
||||
// Otherwise, print "hops away" info, if available
|
||||
else if (hopsAway != CardInfo::HOPS_UNKNOWN) {
|
||||
std::string hopString = to_string(node->hops_away);
|
||||
hopString += " Hop";
|
||||
if (node->hops_away != 1)
|
||||
hopString += "s"; // Append s for "Hops", rather than "Hop"
|
||||
|
||||
printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE);
|
||||
}
|
||||
|
||||
// Print the long name, cropping to prevent overflow onto the right-side info
|
||||
setCrop(0, 0, dividerX - 1, height());
|
||||
printAt(0, lineBY, longName, LEFT, MIDDLE);
|
||||
|
||||
// GFX effect: "hatch" the right edge of longName area
|
||||
// If a longName has been cropped, it will appear to fade out,
|
||||
// creating a soft barrier with the right-side info
|
||||
const int16_t hatchLeft = dividerX - 1 - (fontSmall.lineHeight());
|
||||
const int16_t hatchWidth = fontSmall.lineHeight();
|
||||
hatchRegion(hatchLeft, cardTopY, hatchWidth, cardH, 2, WHITE);
|
||||
|
||||
// Prepare to draw the next card
|
||||
resetCrop();
|
||||
cardTopY += cardH;
|
||||
|
||||
// Once we've run out of screen, stop drawing cards
|
||||
// Depending on tiles / rotation, this may be before we hit maxCards
|
||||
if (cardTopY > height()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw element: a "mobile phone" style signal indicator
|
||||
// We will calculate values as floats, then "rasterize" at the last moment, relative to x and w, etc
|
||||
// This prevents issues with premature rounding when rendering tiny elements
|
||||
void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength strength)
|
||||
{
|
||||
|
||||
/*
|
||||
+-------------------------------------------+
|
||||
| |
|
||||
| |
|
||||
| barHeightRelative=1.0
|
||||
| +--+ ^ |
|
||||
| gutterW +--+ | | | |
|
||||
| <--> +--+ | | | | | |
|
||||
| +--+ | | | | | | | |
|
||||
| | | | | | | | | | |
|
||||
| <-> +--+ +--+ +--+ +--+ v |
|
||||
| paddingW ^ |
|
||||
| paddingH | |
|
||||
| v |
|
||||
+-------------------------------------------+
|
||||
*/
|
||||
|
||||
constexpr float paddingW = 0.1; // Either side
|
||||
constexpr float paddingH = 0.1; // Above and below
|
||||
constexpr float gutterX = 0.1; // Between bars
|
||||
|
||||
constexpr float barHRel[] = {0.3, 0.5, 0.7, 1.0}; // Heights of the signal bars, relative to the talleest
|
||||
constexpr uint8_t barCount = 4; // How many bars we draw. Reference only: changing value won't change the count.
|
||||
|
||||
// Dynamically calculate the width of the bars, and height of the rightmost, relative to other dimensions
|
||||
float barW = (1.0 - (paddingW + ((barCount - 1) * gutterX) + paddingW)) / barCount;
|
||||
float barHMax = 1.0 - (paddingH + paddingH);
|
||||
|
||||
// Draw signal bar rectangles, then placeholder lines once strength reached
|
||||
for (uint8_t i = 0; i < barCount; i++) {
|
||||
// Co-ords for this specific bar
|
||||
float barH = barHMax * barHRel[i];
|
||||
float barX = paddingW + (i * (gutterX + barW));
|
||||
float barY = paddingH + (barHMax - barH);
|
||||
|
||||
// Rasterize to px coords at the last moment
|
||||
int16_t rX = (x + (w * barX)) + 0.5;
|
||||
int16_t rY = (y + (h * barY)) + 0.5;
|
||||
uint16_t rW = (w * barW) + 0.5;
|
||||
uint16_t rH = (h * barH) + 0.5;
|
||||
|
||||
// Draw signal bars, until we are displaying the correct "signal strength", then just draw placeholder lines
|
||||
if (i <= strength)
|
||||
drawRect(rX, rY, rW, rH, BLACK);
|
||||
else {
|
||||
// Just draw a placeholder line
|
||||
float lineY = barY + barH;
|
||||
uint16_t rLineY = (y + (h * lineY)) + 0.5; // Rasterize
|
||||
drawLine(rX, rLineY, rX + rW - 1, rLineY, BLACK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,71 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Base class for Applets which display a list of nodes
|
||||
Used by the "Recents" and "Heard" applets. Possibly more in future?
|
||||
|
||||
+-------------------------------+
|
||||
| | |
|
||||
| SHRT . | | |
|
||||
| Long name 50km |
|
||||
| |
|
||||
| ABCD 2 Hops |
|
||||
| abcdedfghijk 30km |
|
||||
| |
|
||||
+-------------------------------+
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class NodeListApplet : public Applet, public MeshModule
|
||||
{
|
||||
protected:
|
||||
// Info used to draw one card to the node list
|
||||
struct CardInfo {
|
||||
static constexpr uint8_t HOPS_UNKNOWN = -1;
|
||||
static constexpr uint32_t DISTANCE_UNKNOWN = -1;
|
||||
|
||||
NodeNum nodeNum = 0;
|
||||
SignalStrength signal = SignalStrength::SIGNAL_UNKNOWN;
|
||||
uint32_t distanceMeters = DISTANCE_UNKNOWN;
|
||||
uint8_t hopsAway = HOPS_UNKNOWN; // Unknown
|
||||
};
|
||||
|
||||
public:
|
||||
NodeListApplet(const char *name);
|
||||
void onRender() override;
|
||||
|
||||
// MeshModule overrides
|
||||
virtual bool wantPacket(const meshtastic_MeshPacket *p) override;
|
||||
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
protected:
|
||||
virtual void handleParsed(CardInfo c) = 0; // Pass extracted info from a new packet to derived class, for sorting and storage
|
||||
virtual std::string getHeaderText() = 0; // Title for the applet's header. Todo: get this info another way?
|
||||
|
||||
uint8_t maxCards(); // Calculate the maximum number of cards an applet could ever display
|
||||
|
||||
std::deque<CardInfo> cards; // Derived applet places cards here, for this base applet to render
|
||||
|
||||
private:
|
||||
// UI element: a "mobile phone" style signal indicator
|
||||
void drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength signal);
|
||||
|
||||
// Dimensions for drawing
|
||||
// Used for render, and also for maxCards calc
|
||||
const uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards
|
||||
const uint16_t cardH = fontLarge.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,14 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./BasicExampleApplet.h"
|
||||
|
||||
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()
|
||||
{
|
||||
print("Hello, World!");
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,36 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
A bare-minimum example of an InkHUD applet.
|
||||
Only prints Hello World.
|
||||
|
||||
In variants/<your device>/nicheGraphics.h:
|
||||
|
||||
- include this .h file
|
||||
- add the following line of code:
|
||||
windowManager->addApplet("Basic", new InkHUD::BasicExampleApplet);
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class BasicExampleApplet : public Applet
|
||||
{
|
||||
public:
|
||||
// You must have an onRender() method
|
||||
// All drawing happens here
|
||||
|
||||
void onRender() override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,54 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./NewMsgExampleApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
// We configured MeshModule API to call this method when we receive a new text message
|
||||
ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
|
||||
// Abort if applet fully deactivated
|
||||
if (!isActive())
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Check that this is an incoming message
|
||||
// Outgoing messages (sent by us) will also call handleReceived
|
||||
|
||||
if (!isFromUs(&mp)) {
|
||||
// Store the sender's nodenum
|
||||
// We need to keep this information, so we can re-use it anytime render() is called
|
||||
haveMessage = true;
|
||||
fromWho = mp.from;
|
||||
|
||||
// Tell InkHUD that we have something new to show on the screen
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
// Tell MeshModule API to continue informing other firmware components about this message
|
||||
// We're not the only component which is interested in new text messages
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// All drawing happens here
|
||||
// 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()
|
||||
{
|
||||
setFont(fontSmall);
|
||||
|
||||
printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0)
|
||||
|
||||
int16_t centerX = X(0.5); // Same as width() / 2
|
||||
int16_t centerY = Y(0.5); // Same as height() / 2
|
||||
|
||||
if (haveMessage) {
|
||||
printAt(centerX, centerY, "New Message", CENTER, BOTTOM);
|
||||
printAt(centerX, centerY, "From: " + hexifyNodeNum(fromWho), CENTER, TOP);
|
||||
} else {
|
||||
printAt(centerX, centerY, "No Message", CENTER, MIDDLE); // Place center of string at (centerX, centerY)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,61 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
An example of an InkHUD applet.
|
||||
Tells us when a new text message arrives.
|
||||
|
||||
This applet makes use of the MeshModule API to detect new messages,
|
||||
which is a general part of the Meshtastic firmware, and not part of InkHUD.
|
||||
|
||||
In variants/<your device>/nicheGraphics.h:
|
||||
|
||||
- include this .h file
|
||||
- add the following line of code:
|
||||
windowManager->addApplet("New Msg", new InkHUD::NewMsgExampleApplet);
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
#include "mesh/SinglePortModule.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class NewMsgExampleApplet : public Applet, public SinglePortModule
|
||||
{
|
||||
public:
|
||||
// The MeshModule API requires us to have a constructor, to specify that we're interested in Text Messages.
|
||||
NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {}
|
||||
|
||||
// All drawing happens here
|
||||
void onRender() override;
|
||||
|
||||
// Your applet might also want to use some of these
|
||||
// Useful for setting up or tidying up
|
||||
|
||||
/*
|
||||
void onActivate(); // When started
|
||||
void onDeactivate(); // When stopped
|
||||
void onForeground(); // When shown by short-press
|
||||
void onBackground(); // When hidden by short-press
|
||||
*/
|
||||
|
||||
private:
|
||||
// Called when we receive new text messages
|
||||
// Part of the MeshModule API
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
// Store info from handleReceived
|
||||
bool haveMessage = false;
|
||||
NodeNum fromWho;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,107 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./BatteryIconApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::BatteryIconApplet::onActivate()
|
||||
{
|
||||
// Show at boot, if user has previously enabled the feature
|
||||
if (settings.optionalFeatures.batteryIcon)
|
||||
bringToForeground();
|
||||
|
||||
// Register to our have BatteryIconApplet::onPowerStatusUpdate method called when new power info is available
|
||||
// This happens whether or not the battery icon feature is enabled
|
||||
powerStatusObserver.observe(&powerStatus->onNewStatus);
|
||||
}
|
||||
|
||||
void InkHUD::BatteryIconApplet::onDeactivate()
|
||||
{
|
||||
// Stop having onPowerStatusUpdate called
|
||||
powerStatusObserver.unobserve(&powerStatus->onNewStatus);
|
||||
}
|
||||
|
||||
// We handle power status' even when the feature is disabled,
|
||||
// so that we have up to date data ready if the feature is enabled later.
|
||||
// Otherwise could be 30s before new status update, with weird battery value displayed
|
||||
int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *status)
|
||||
{
|
||||
// System applets are always active
|
||||
assert(isActive());
|
||||
|
||||
// This method should only receive power statuses
|
||||
// If we get a different type of status, something has gone weird elsewhere
|
||||
assert(status->getStatusType() == STATUS_TYPE_POWER);
|
||||
|
||||
meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status;
|
||||
|
||||
// Get the new state of charge %, and round to the nearest 10%
|
||||
uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10;
|
||||
|
||||
// If rounded value has changed, trigger a display update
|
||||
// It's okay to requestUpdate before we store the new value, as the update won't run until next loop()
|
||||
// Don't trigger an update if the feature is disabled
|
||||
if (this->socRounded != newSocRounded && settings.optionalFeatures.batteryIcon)
|
||||
requestUpdate();
|
||||
|
||||
// Store the new value
|
||||
this->socRounded = newSocRounded;
|
||||
|
||||
return 0; // Tell Observable to continue informing other observers
|
||||
}
|
||||
|
||||
void InkHUD::BatteryIconApplet::onRender()
|
||||
{
|
||||
// Fill entire tile
|
||||
// - size of icon controlled by size of tile
|
||||
int16_t l = 0;
|
||||
int16_t t = 0;
|
||||
uint16_t w = width();
|
||||
int16_t h = height();
|
||||
|
||||
// Clear the region beneath the tile
|
||||
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
||||
// We do need to do this with the battery though, as it is an "overlay"
|
||||
fillRect(l, t, w, h, WHITE);
|
||||
|
||||
// Vertical centerline
|
||||
const int16_t m = t + (h / 2);
|
||||
|
||||
// =====================
|
||||
// Draw battery outline
|
||||
// =====================
|
||||
|
||||
// Positive terminal "bump"
|
||||
const int16_t &bumpL = l;
|
||||
const uint16_t bumpH = h / 2;
|
||||
const int16_t bumpT = m - (bumpH / 2);
|
||||
constexpr uint16_t bumpW = 2;
|
||||
fillRect(bumpL, bumpT, bumpW, bumpH, BLACK);
|
||||
|
||||
// Main body of battery
|
||||
const int16_t bodyL = bumpL + bumpW;
|
||||
const int16_t &bodyT = t;
|
||||
const int16_t &bodyH = h;
|
||||
const int16_t bodyW = w - bumpW;
|
||||
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
|
||||
|
||||
// Erase join between bump and body
|
||||
drawLine(bodyL, bumpT, bodyL, bumpT + bumpH - 1, WHITE);
|
||||
|
||||
// ===================
|
||||
// Draw battery level
|
||||
// ===================
|
||||
|
||||
constexpr int16_t slicePad = 2;
|
||||
const int16_t sliceL = bodyL + slicePad;
|
||||
const int16_t sliceT = bodyT + slicePad;
|
||||
const uint16_t sliceH = bodyH - (slicePad * 2);
|
||||
uint16_t sliceW = bodyW - (slicePad * 2);
|
||||
|
||||
sliceW = (sliceW * socRounded) / 100; // Apply percentage
|
||||
|
||||
hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK);
|
||||
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,41 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
This applet floats top-left, giving a graphical representation of battery remaining
|
||||
It should be optional, enabled by the on-screen menu
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
#include "PowerStatus.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class BatteryIconApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
|
||||
int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available
|
||||
|
||||
protected:
|
||||
// Get informed when new information about the battery is available (via onPowerStatusUpdate method)
|
||||
CallbackObserver<BatteryIconApplet, const meshtastic::Status *> powerStatusObserver =
|
||||
CallbackObserver<BatteryIconApplet, const meshtastic::Status *>(this, &BatteryIconApplet::onPowerStatusUpdate);
|
||||
|
||||
uint8_t socRounded = 0; // Battery state of charge, rounded to nearest 10%
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,108 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./LogoApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet")
|
||||
{
|
||||
// Don't autostart the runOnce() timer
|
||||
OSThread::disable();
|
||||
|
||||
// Grab the WindowManager singleton, for convenience
|
||||
windowManager = WindowManager::getInstance();
|
||||
}
|
||||
|
||||
void InkHUD::LogoApplet::onRender()
|
||||
{
|
||||
// Size of the region which the logo should "scale to fit"
|
||||
uint16_t logoWLimit = X(0.8);
|
||||
uint16_t logoHLimit = Y(0.5);
|
||||
|
||||
// Get the max width and height we can manage within the region, while still maintaining aspect ratio
|
||||
uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit);
|
||||
uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit);
|
||||
|
||||
// Where to place the center of the logo
|
||||
int16_t logoCX = X(0.5);
|
||||
int16_t logoCY = Y(0.5 - 0.05);
|
||||
|
||||
drawLogo(logoCX, logoCY, logoW, logoH);
|
||||
|
||||
if (!textLeft.empty()) {
|
||||
setFont(fontSmall);
|
||||
printAt(0, 0, textLeft, LEFT, TOP);
|
||||
}
|
||||
|
||||
if (!textRight.empty()) {
|
||||
setFont(fontSmall);
|
||||
printAt(X(1), 0, textRight, RIGHT, TOP);
|
||||
}
|
||||
|
||||
if (!textTitle.empty()) {
|
||||
int16_t logoB = logoCY + (logoH / 2); // Bottom of the logo
|
||||
setFont(fontTitle);
|
||||
printAt(X(0.5), logoB + Y(0.1), textTitle, CENTER, TOP);
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::LogoApplet::onForeground()
|
||||
{
|
||||
// If another applet has locked the display, ask it to exit
|
||||
Applet *other = windowManager->whoLocked();
|
||||
if (other != nullptr)
|
||||
other->sendToBackground();
|
||||
|
||||
windowManager->claimFullscreen(this); // Take ownership of fullscreen tile
|
||||
windowManager->lock(this); // Prevent other applets from requesting updates
|
||||
}
|
||||
|
||||
void InkHUD::LogoApplet::onBackground()
|
||||
{
|
||||
OSThread::disable(); // Disable auto-dismiss timer, in case applet was dismissed early (sendToBackground from outside class)
|
||||
|
||||
windowManager->releaseFullscreen(); // Relinquish ownership of fullscreen tile
|
||||
windowManager->unlock(this); // Allow normal user applet update requests to resume
|
||||
|
||||
// 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
|
||||
windowManager->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
int32_t InkHUD::LogoApplet::runOnce()
|
||||
{
|
||||
LOG_DEBUG("Sent to background by timer");
|
||||
sendToBackground();
|
||||
return OSThread::disable();
|
||||
}
|
||||
|
||||
// Begin displaying the screen which is shown at startup
|
||||
// Suggest EInk::await after calling this method
|
||||
void InkHUD::LogoApplet::showBootScreen()
|
||||
{
|
||||
OSThread::setIntervalFromNow(8 * 1000UL);
|
||||
OSThread::enabled = true;
|
||||
|
||||
textLeft = "";
|
||||
textRight = "";
|
||||
textTitle = xstr(APP_VERSION_SHORT);
|
||||
fontTitle = fontSmall;
|
||||
|
||||
bringToForeground();
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Already requested, just upgrading to FULL
|
||||
}
|
||||
|
||||
// Begin displaying the screen which is shown at shutdown
|
||||
// Needs EInk::await after calling this method, to ensure display updates before shutdown
|
||||
void InkHUD::LogoApplet::showShutdownScreen()
|
||||
{
|
||||
textLeft = "";
|
||||
textRight = "";
|
||||
textTitle = owner.short_name;
|
||||
fontTitle = fontLarge;
|
||||
|
||||
bringToForeground();
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Already requested, just upgrading to FULL
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,47 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Shows the Meshtastic logo fullscreen, with accompanying text
|
||||
Used for boot and shutdown
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class LogoApplet : public Applet, public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
LogoApplet();
|
||||
void onRender() override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
|
||||
// Note: interacting directly with an applet like this is non-standard
|
||||
// Only permitted because this is a "system applet", which has special behavior and interacts directly with WindowManager
|
||||
|
||||
void showBootScreen();
|
||||
void showShutdownScreen();
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override;
|
||||
|
||||
std::string textLeft;
|
||||
std::string textRight;
|
||||
std::string textTitle;
|
||||
AppletFont fontTitle;
|
||||
|
||||
WindowManager *windowManager = nullptr; // For convenience
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,38 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Set of end-point actions for the Menu Applet
|
||||
|
||||
Added as menu entries in MenuApplet::showPage
|
||||
Behaviors assigned in MenuApplet::execute
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
enum MenuAction {
|
||||
NO_ACTION,
|
||||
SEND_NODEINFO,
|
||||
SEND_POSITION,
|
||||
SHUTDOWN,
|
||||
NEXT_TILE,
|
||||
TOGGLE_APPLET,
|
||||
ACTIVATE_APPLETS, // Todo: remove? Possible redundant, handled by TOGGLE_APPLET?
|
||||
TOGGLE_AUTOSHOW_APPLET,
|
||||
SET_RECENTS,
|
||||
ROTATE,
|
||||
LAYOUT,
|
||||
TOGGLE_BATTERY_ICON,
|
||||
TOGGLE_NOTIFICATIONS,
|
||||
TOGGLE_BACKLIGHT,
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,612 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./MenuApplet.h"
|
||||
|
||||
#include "PowerStatus.h"
|
||||
#include "RTC.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
static constexpr uint8_t MENU_TIMEOUT_SEC = 60; // How many seconds before menu auto-closes
|
||||
|
||||
// Options for the "Recents" menu
|
||||
// These are offered to users as possible values for settings.recentlyActiveSeconds
|
||||
static constexpr uint8_t RECENTS_OPTIONS_MINUTES[] = {2, 5, 10, 30, 60, 120};
|
||||
|
||||
InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet")
|
||||
{
|
||||
// No timer tasks at boot
|
||||
OSThread::disable();
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onActivate()
|
||||
{
|
||||
// Grab pointers to some singleton components which the menu interacts with
|
||||
// We could do this every time we needed them, in place,
|
||||
// but this just makes the code tidier
|
||||
|
||||
this->windowManager = WindowManager::getInstance();
|
||||
|
||||
// Note: don't get instance if we're not actually using the backlight,
|
||||
// or else you will unintentionally instantiate it
|
||||
if (settings.optionalMenuItems.backlight) {
|
||||
backlight = Drivers::LatchingBacklight::getInstance();
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onForeground()
|
||||
{
|
||||
// We do need this before we render, but we can optimize by just calculating it once now
|
||||
systemInfoPanelHeight = getSystemInfoPanelHeight();
|
||||
|
||||
// Display initial menu page
|
||||
showPage(MenuPage::ROOT);
|
||||
|
||||
// If device has a backlight which isn't controlled by aux button:
|
||||
// backlight on always when menu opens.
|
||||
// Courtesy to T-Echo users who removed the capacitive touch button
|
||||
if (settings.optionalMenuItems.backlight) {
|
||||
assert(backlight);
|
||||
if (!backlight->isOn())
|
||||
backlight->peek();
|
||||
}
|
||||
|
||||
// Prevent user applets requested update while menu is open
|
||||
windowManager->lock(this);
|
||||
|
||||
// Begin the auto-close timeout
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
OSThread::enabled = true;
|
||||
|
||||
// Upgrade the refresh to FAST, for guaranteed responsiveness
|
||||
windowManager->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onBackground()
|
||||
{
|
||||
// If device has a backlight which isn't controlled by aux button:
|
||||
// Item in options submenu allows keeping backlight on after menu is closed
|
||||
// If this item is deselected we will turn backlight off again, now that menu is closing
|
||||
if (settings.optionalMenuItems.backlight) {
|
||||
assert(backlight);
|
||||
if (!backlight->isLatched())
|
||||
backlight->off();
|
||||
}
|
||||
|
||||
// Stop the auto-timeout
|
||||
OSThread::disable();
|
||||
|
||||
// Resume normal rendering and button behavior of user applets
|
||||
windowManager->unlock(this);
|
||||
|
||||
// Restore the user applet whose tile we borrowed
|
||||
if (borrowedTileOwner)
|
||||
borrowedTileOwner->bringToForeground();
|
||||
Tile *t = getTile();
|
||||
t->assignApplet(borrowedTileOwner); // Break our link with the tile, (and relink it with real owner, if it had one)
|
||||
borrowedTileOwner = nullptr;
|
||||
|
||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||
// We're only updating here to ugrade from UNSPECIFIED to FAST, to ensure responsiveness when exiting menu
|
||||
windowManager->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
// Open the menu
|
||||
// Parameter specifies which user-tile the menu will use
|
||||
// The user applet originally on this tile will be restored when the menu closes
|
||||
void InkHUD::MenuApplet::show(Tile *t)
|
||||
{
|
||||
// Remember who *really* owns this tile
|
||||
borrowedTileOwner = t->getAssignedApplet();
|
||||
|
||||
// Hide the owner, if it is a valid applet
|
||||
if (borrowedTileOwner)
|
||||
borrowedTileOwner->sendToBackground();
|
||||
|
||||
// Break the owner's link with tile
|
||||
// Relink it to menu applet
|
||||
t->assignApplet(this);
|
||||
|
||||
// Show menu
|
||||
bringToForeground();
|
||||
}
|
||||
|
||||
// Auto-exit the menu applet after a period of inactivity
|
||||
// The values shown on the root menu are only a snapshot: they are not re-rendered while the menu remains open.
|
||||
// By exiting the menu, we prevent users mistakenly believing that the data will update.
|
||||
int32_t InkHUD::MenuApplet::runOnce()
|
||||
{
|
||||
// runOnce's interval is pushed back when a button is pressed
|
||||
// If we do actually run, it means no button input occurred within MENU_TIMEOUT_SEC,
|
||||
// so we close the menu.
|
||||
showPage(EXIT);
|
||||
|
||||
// Timer should disable after firing
|
||||
// This is redundant, as onBackground() will also disable
|
||||
return OSThread::disable();
|
||||
}
|
||||
|
||||
// Perform action for a menu item, then change page
|
||||
// Behaviors for MenuActions are defined here
|
||||
void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
{
|
||||
// Perform an action
|
||||
// ------------------
|
||||
switch (item.action) {
|
||||
|
||||
// Open a submenu without performing any action
|
||||
// Also handles exit
|
||||
case NO_ACTION:
|
||||
break;
|
||||
|
||||
case NEXT_TILE:
|
||||
// Note performed manually;
|
||||
// WindowManager::nextTile is raised by aux button press only, and will interact poorly with the menu
|
||||
settings.userTiles.focused = (settings.userTiles.focused + 1) % settings.userTiles.count;
|
||||
windowManager->changeLayout();
|
||||
cursor = 0; // No menu item selected, for quick exit after tile swap
|
||||
cursorShown = false;
|
||||
break;
|
||||
|
||||
case ROTATE:
|
||||
settings.rotation = (settings.rotation + 1) % 4;
|
||||
windowManager->changeLayout();
|
||||
// requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Would update regardless; just selecting FULL
|
||||
break;
|
||||
|
||||
case LAYOUT:
|
||||
// Todo: smarter incrementing of tile count
|
||||
settings.userTiles.count++;
|
||||
|
||||
if (settings.userTiles.count == 3) // Skip 3 tiles: not done yet
|
||||
settings.userTiles.count++;
|
||||
|
||||
if (settings.userTiles.count > settings.userTiles.maxCount) // Loop around if tile count now too high
|
||||
settings.userTiles.count = 1;
|
||||
|
||||
windowManager->changeLayout();
|
||||
// requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Would update regardless; just selecting FULL
|
||||
break;
|
||||
|
||||
case TOGGLE_APPLET:
|
||||
settings.userApplets.active[cursor] = !settings.userApplets.active[cursor];
|
||||
windowManager->changeActivatedApplets();
|
||||
// requestUpdate(Drivers::EInk::UpdateTypes::FULL); // Select FULL, seeing how this action doesn't auto exit
|
||||
break;
|
||||
|
||||
case ACTIVATE_APPLETS:
|
||||
// Todo: remove this action? Already handled by TOGGLE_APPLET?
|
||||
windowManager->changeActivatedApplets();
|
||||
break;
|
||||
|
||||
case TOGGLE_AUTOSHOW_APPLET:
|
||||
// Toggle settings.userApplets.autoshow[] value, via MenuItem::checkState pointer set in populateAutoshowPage()
|
||||
*items.at(cursor).checkState = !(*items.at(cursor).checkState);
|
||||
break;
|
||||
|
||||
case TOGGLE_NOTIFICATIONS:
|
||||
settings.optionalFeatures.notifications = !settings.optionalFeatures.notifications;
|
||||
break;
|
||||
|
||||
case SET_RECENTS:
|
||||
// Set value of settings.recentlyActiveSeconds
|
||||
// Uses menu cursor to read RECENTS_OPTIONS_MINUTES array (defined at top of this file)
|
||||
assert(cursor < sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]));
|
||||
settings.recentlyActiveSeconds = RECENTS_OPTIONS_MINUTES[cursor] * 60; // Menu items are in minutes
|
||||
break;
|
||||
|
||||
case SHUTDOWN:
|
||||
LOG_INFO("Shutting down from menu");
|
||||
power->shutdown();
|
||||
// Menu is then sent to background via onShutdown
|
||||
break;
|
||||
|
||||
case TOGGLE_BATTERY_ICON:
|
||||
windowManager->toggleBatteryIcon();
|
||||
break;
|
||||
|
||||
case TOGGLE_BACKLIGHT:
|
||||
// Note: backlight is already on in this situation
|
||||
// We're marking that it should *remain* on once menu closes
|
||||
assert(backlight);
|
||||
if (backlight->isLatched())
|
||||
backlight->off();
|
||||
else
|
||||
backlight->latch();
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG_WARN("Action not implemented");
|
||||
}
|
||||
|
||||
// Move to next page, as defined for the MenuItem
|
||||
showPage(item.nextPage);
|
||||
}
|
||||
|
||||
// Display a new page of MenuItems
|
||||
// May reload same page, or exit menu applet entirely
|
||||
// Fills the MenuApplet::items vector
|
||||
void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
{
|
||||
items.clear();
|
||||
|
||||
switch (page) {
|
||||
case ROOT:
|
||||
// Optional: next applet
|
||||
if (settings.optionalMenuItems.nextTile && settings.userTiles.count > 1)
|
||||
items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown
|
||||
|
||||
// items.push_back(MenuItem("Send", MenuPage::SEND)); // TODO
|
||||
items.push_back(MenuItem("Options", MenuPage::OPTIONS));
|
||||
// items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO
|
||||
items.push_back(MenuItem("Save & Shutdown", MenuAction::SHUTDOWN));
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
break;
|
||||
|
||||
case SEND:
|
||||
items.push_back(MenuItem("Send Message", MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Send NodeInfo", MenuAction::SEND_NODEINFO));
|
||||
items.push_back(MenuItem("Send Position", MenuAction::SEND_POSITION));
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
break;
|
||||
|
||||
case OPTIONS:
|
||||
// Optional: backlight
|
||||
if (settings.optionalMenuItems.backlight) {
|
||||
assert(backlight);
|
||||
items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label
|
||||
MenuAction::TOGGLE_BACKLIGHT, // Action
|
||||
MenuPage::EXIT // Exit once complete
|
||||
));
|
||||
}
|
||||
|
||||
items.push_back(MenuItem("Applets", MenuPage::APPLETS));
|
||||
items.push_back(MenuItem("Auto-show", MenuPage::AUTOSHOW));
|
||||
items.push_back(MenuItem("Recents Duration", MenuPage::RECENTS));
|
||||
if (settings.userTiles.maxCount > 1)
|
||||
items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS));
|
||||
items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS));
|
||||
items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS,
|
||||
&settings.optionalFeatures.notifications));
|
||||
items.push_back(
|
||||
MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS, &settings.optionalFeatures.batteryIcon));
|
||||
|
||||
// TODO - GPS and Wifi switches
|
||||
/*
|
||||
// Optional: has GPS
|
||||
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED)
|
||||
items.push_back(MenuItem("Enable GPS", MenuPage::EXIT)); // TODO
|
||||
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED)
|
||||
items.push_back(MenuItem("Disable GPS", MenuPage::EXIT)); // TODO
|
||||
|
||||
// Optional: using wifi
|
||||
if (!config.bluetooth.enabled)
|
||||
items.push_back(MenuItem("Enable Bluetooth", MenuPage::EXIT)); // TODO: escape hatch if wifi configured wrong
|
||||
*/
|
||||
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
break;
|
||||
|
||||
case APPLETS:
|
||||
populateAppletPage();
|
||||
items.push_back(MenuItem("Exit", MenuAction::ACTIVATE_APPLETS));
|
||||
break;
|
||||
|
||||
case AUTOSHOW:
|
||||
populateAutoshowPage();
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
break;
|
||||
|
||||
case RECENTS:
|
||||
populateRecentsPage();
|
||||
break;
|
||||
|
||||
case EXIT:
|
||||
sendToBackground(); // Menu applet dismissed, allow normal behavior to resume
|
||||
// requestUpdate(Drivers::EInk::UpdateTypes::FULL);
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG_WARN("Page not implemented");
|
||||
}
|
||||
|
||||
// Reset the cursor, unless reloading same page
|
||||
// (or now out-of-bounds)
|
||||
if (page != currentPage || cursor >= items.size()) {
|
||||
cursor = 0;
|
||||
|
||||
// ROOT menu has special handling: unselected at first, to emphasise the system info panel
|
||||
if (page == ROOT)
|
||||
cursorShown = false;
|
||||
}
|
||||
|
||||
// Remember which page we are on now
|
||||
currentPage = page;
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onRender()
|
||||
{
|
||||
if (items.size() == 0)
|
||||
LOG_ERROR("Empty Menu");
|
||||
|
||||
// Testing only
|
||||
setFont(fontSmall);
|
||||
|
||||
// Dimensions for the slots where we will draw menuItems
|
||||
const float padding = 0.05;
|
||||
const uint16_t itemH = fontSmall.lineHeight() * 2;
|
||||
const int16_t itemW = width() - X(padding) - X(padding);
|
||||
const int16_t itemL = X(padding);
|
||||
const int16_t itemR = X(1 - padding);
|
||||
int16_t itemT = 0; // Top (y px of current slot). Incremented as we draw. Adjusted to fit system info panel on ROOT menu.
|
||||
|
||||
// How many full menuItems will fit on screen
|
||||
uint8_t slotCount = (height() - itemT) / itemH;
|
||||
|
||||
// System info panel at the top of the menu
|
||||
// =========================================
|
||||
|
||||
uint16_t &siH = systemInfoPanelHeight; // System info - height. Calculated at onForeground
|
||||
const uint8_t slotsObscured = ceilf(siH / (float)itemH); // How many slots are obscured by system info panel
|
||||
|
||||
// System info - top
|
||||
// Remain at 0px, until cursor reaches bottom of screen, then begin to scroll off screen.
|
||||
// This is the same behavior we expect from the non-root menus.
|
||||
// Implementing this with the systemp panel is slightly annoying though,
|
||||
// and required adding the MenuApplet::getSystemInfoPanelHeight method
|
||||
int16_t siT;
|
||||
if (cursor < slotCount - slotsObscured - 1) // (Minus 1: comparing zero based index with a count)
|
||||
siT = 0;
|
||||
else
|
||||
siT = 0 - ((cursor - (slotCount - slotsObscured - 1)) * itemH);
|
||||
|
||||
// If showing ROOT menu,
|
||||
// and the panel isn't yet scrolled off screen top
|
||||
if (currentPage == ROOT) {
|
||||
drawSystemInfoPanel(0, siT, width()); // Draw the panel.
|
||||
itemT = max(siT + siH, 0); // Offset the first menu entry, so menu starts below the system info panel
|
||||
}
|
||||
|
||||
// Draw menu items
|
||||
// ===================
|
||||
|
||||
// Which item will be drawn to the top-most slot?
|
||||
// Initially, this is the item 0, but may increase once we begin scrolling
|
||||
uint8_t firstItem;
|
||||
if (cursor < slotCount)
|
||||
firstItem = 0;
|
||||
else
|
||||
firstItem = cursor - (slotCount - 1);
|
||||
|
||||
// Which item will be drawn to the bottom-most slot?
|
||||
// This may be beyond the slot-count, to draw a partially off-screen item below the bottom-most slow
|
||||
// This may be less than the slot-count, if we are reaching the end of the menuItems
|
||||
uint8_t lastItem = min((uint8_t)firstItem + slotCount, (uint8_t)items.size() - 1);
|
||||
|
||||
// -- Loop: draw each (visible) menu item --
|
||||
for (uint8_t i = firstItem; i <= lastItem; i++) {
|
||||
// Grab the menuItem
|
||||
MenuItem item = items.at(i);
|
||||
|
||||
// Center-line for the text
|
||||
int16_t center = itemT + (itemH / 2);
|
||||
|
||||
if (cursorShown && i == cursor)
|
||||
drawRect(itemL, itemT, itemW, itemH, BLACK);
|
||||
printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE);
|
||||
|
||||
// Testing only: circle instead of check box
|
||||
if (item.checkState) {
|
||||
const uint16_t cbWH = fontSmall.lineHeight(); // Checbox: width / height
|
||||
const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left
|
||||
const int16_t cbT = center - (cbWH / 2); // Checkbox : top
|
||||
// Checkbox ticked
|
||||
if (*(item.checkState)) {
|
||||
drawRect(cbL, cbT, cbWH, cbWH, BLACK);
|
||||
// First point of tick: pen down
|
||||
const int16_t t1Y = center;
|
||||
const int16_t t1X = cbL + 3;
|
||||
// Second point of tick: base
|
||||
const int16_t t2Y = center + (cbWH / 2) - 2;
|
||||
const int16_t t2X = cbL + (cbWH / 2);
|
||||
// Third point of tick: end of tail
|
||||
const int16_t t3Y = center - (cbWH / 2) - 2;
|
||||
const int16_t t3X = cbL + cbWH + 2;
|
||||
// Draw twice: faux bold
|
||||
drawLine(t1X, t1Y, t2X, t2Y, BLACK);
|
||||
drawLine(t2X, t2Y, t3X, t3Y, BLACK);
|
||||
drawLine(t1X + 1, t1Y, t2X + 1, t2Y, BLACK);
|
||||
drawLine(t2X + 1, t2Y, t3X + 1, t3Y, BLACK);
|
||||
}
|
||||
// Checkbox ticked
|
||||
else
|
||||
drawRect(cbL, cbT, cbWH, cbWH, BLACK);
|
||||
}
|
||||
|
||||
// Increment the y value (top) as we go
|
||||
itemT += itemH;
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onButtonShortPress()
|
||||
{
|
||||
// Push the auto-close timer back
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
// Move menu cursor to next entry, then update
|
||||
if (cursorShown)
|
||||
cursor = (cursor + 1) % items.size();
|
||||
else
|
||||
cursorShown = true;
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onButtonLongPress()
|
||||
{
|
||||
// Push the auto-close timer back
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
if (cursorShown)
|
||||
execute(items.at(cursor));
|
||||
else
|
||||
showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close
|
||||
|
||||
// If we didn't already request a specialized update, when handling a menu action,
|
||||
// then perform the usual fast update.
|
||||
// FAST keeps things responsive: important because we're dealing with user input
|
||||
if (!wantsToRender())
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
// Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu
|
||||
void InkHUD::MenuApplet::populateAppletPage()
|
||||
{
|
||||
assert(items.size() == 0);
|
||||
|
||||
for (uint8_t i = 0; i < windowManager->getAppletCount(); i++) {
|
||||
const char *name = windowManager->getAppletName(i);
|
||||
bool *isActive = &(settings.userApplets.active[i]);
|
||||
items.push_back(MenuItem(name, MenuAction::TOGGLE_APPLET, MenuPage::APPLETS, isActive));
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamically create MenuItem entries for selecting which applets will automatically come to foreground when they have new data
|
||||
// We only populate this menu page with applets which are actually active
|
||||
// We use the MenuItem::checkState pointer to toggle the setting in MenuApplet::execute. Bit of a hack, but convenient.
|
||||
void InkHUD::MenuApplet::populateAutoshowPage()
|
||||
{
|
||||
assert(items.size() == 0);
|
||||
|
||||
for (uint8_t i = 0; i < windowManager->getAppletCount(); i++) {
|
||||
// Only add a menu item if applet is active
|
||||
if (settings.userApplets.active[i]) {
|
||||
const char *name = windowManager->getAppletName(i);
|
||||
bool *isActive = &(settings.userApplets.autoshow[i]);
|
||||
items.push_back(MenuItem(name, MenuAction::TOGGLE_AUTOSHOW_APPLET, MenuPage::AUTOSHOW, isActive));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::populateRecentsPage()
|
||||
{
|
||||
// How many values are shown for use to choose from
|
||||
constexpr uint8_t optionCount = sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]);
|
||||
|
||||
// Create an entry for each item in RECENTS_OPTIONS_MINUTES array
|
||||
// (Defined at top of this file)
|
||||
for (uint8_t i = 0; i < optionCount; i++) {
|
||||
std::string label = to_string(RECENTS_OPTIONS_MINUTES[i]) + " mins";
|
||||
items.push_back(MenuItem(label.c_str(), MenuAction::SET_RECENTS, MenuPage::EXIT));
|
||||
}
|
||||
}
|
||||
|
||||
// Renders the panel shown at the top of the root menu.
|
||||
// Displays the clock, and several other pieces of instantaneous system info,
|
||||
// which we'd prefer not to have displayed in a normal applet, as they update too frequently.
|
||||
void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, uint16_t *renderedHeight)
|
||||
{
|
||||
// Reset the height
|
||||
// We'll add to this as we add elements
|
||||
uint16_t height = 0;
|
||||
|
||||
// Clock (potentially)
|
||||
// ====================
|
||||
std::string clockString = getTimeString();
|
||||
if (clockString.length() > 0) {
|
||||
setFont(fontLarge);
|
||||
printAt(width / 2, top, clockString, CENTER, TOP);
|
||||
|
||||
height += fontLarge.lineHeight();
|
||||
height += fontLarge.lineHeight() * 0.1; // Padding below clock
|
||||
}
|
||||
|
||||
// Stats
|
||||
// ===================
|
||||
|
||||
setFont(fontSmall);
|
||||
|
||||
// Position of the label row for the system info
|
||||
const int16_t labelT = top + height;
|
||||
height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing
|
||||
|
||||
// Position of the data row for the system info
|
||||
const int16_t valT = top + height;
|
||||
height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing (between bottom line and divider)
|
||||
|
||||
// Position of divider between the info panel and the menu entries
|
||||
const int16_t divY = top + height;
|
||||
height += fontSmall.lineHeight() * 0.2; // Padding *below* the divider. (Above first menu item)
|
||||
|
||||
// Create a variable number of columns
|
||||
// Either 3 or 4, depending on whether we have GPS
|
||||
// Todo
|
||||
constexpr uint8_t N_COL = 3;
|
||||
int16_t colL[N_COL];
|
||||
int16_t colC[N_COL];
|
||||
int16_t colR[N_COL];
|
||||
for (uint8_t i = 0; i < N_COL; i++) {
|
||||
colL[i] = left + ((width / N_COL) * i);
|
||||
colC[i] = colL[i] + ((width / N_COL) / 2);
|
||||
colR[i] = colL[i] + (width / N_COL);
|
||||
}
|
||||
|
||||
// Info blocks, left to right
|
||||
|
||||
// Voltage
|
||||
float voltage = powerStatus->getBatteryVoltageMv() / 1000.0;
|
||||
char voltageStr[6]; // "XX.XV"
|
||||
sprintf(voltageStr, "%.1fV", voltage);
|
||||
printAt(colC[0], labelT, "Bat", CENTER, TOP);
|
||||
printAt(colC[0], valT, voltageStr, CENTER, TOP);
|
||||
|
||||
// Divider
|
||||
for (int16_t y = valT; y <= divY; y += 3)
|
||||
drawPixel(colR[0], y, BLACK);
|
||||
|
||||
// Channel Util
|
||||
char chUtilStr[4]; // "XX%"
|
||||
sprintf(chUtilStr, "%2.f%%", airTime->channelUtilizationPercent());
|
||||
printAt(colC[1], labelT, "Ch", CENTER, TOP);
|
||||
printAt(colC[1], valT, chUtilStr, CENTER, TOP);
|
||||
|
||||
// Divider
|
||||
for (int16_t y = valT; y <= divY; y += 3)
|
||||
drawPixel(colR[1], y, BLACK);
|
||||
|
||||
// Duty Cycle (AirTimeTx)
|
||||
char dutyUtilStr[4]; // "XX%"
|
||||
sprintf(dutyUtilStr, "%2.f%%", airTime->utilizationTXPercent());
|
||||
printAt(colC[2], labelT, "Duty", CENTER, TOP);
|
||||
printAt(colC[2], valT, dutyUtilStr, CENTER, TOP);
|
||||
|
||||
/*
|
||||
// Divider
|
||||
for (int16_t y = valT; y <= divY; y += 3)
|
||||
drawPixel(colR[2], y, BLACK);
|
||||
|
||||
// GPS satellites - todo
|
||||
printAt(colC[3], labelT, "Sats", CENTER, TOP);
|
||||
printAt(colC[3], valT, "ToDo", CENTER, TOP);
|
||||
*/
|
||||
|
||||
// Horizontal divider, at bottom of system info panel
|
||||
for (int16_t x = 0; x < width; x += 2) // Divider, centered in the padding between first system panel and first item
|
||||
drawPixel(x, divY, BLACK);
|
||||
|
||||
if (renderedHeight != nullptr)
|
||||
*renderedHeight = height;
|
||||
}
|
||||
|
||||
// Get the height of the the panel drawn at the top of the menu
|
||||
// This is inefficient, as we do actually have to render the panel to determine the height
|
||||
// It solves a catch-22 situtation, where slotCount needs to know panel height, and panel height needs to know slotCount
|
||||
uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight()
|
||||
{
|
||||
// Render *waay* off screen
|
||||
uint16_t height = 0;
|
||||
drawSystemInfoPanel(INT16_MIN, INT16_MIN, 1, &height);
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,60 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h"
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
#include "graphics/niche/InkHUD/WindowManager.h"
|
||||
|
||||
#include "./MenuItem.h"
|
||||
#include "./MenuPage.h"
|
||||
|
||||
#include "concurrency/OSThread.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class Applet;
|
||||
|
||||
class MenuApplet : public Applet, public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
MenuApplet();
|
||||
void onActivate() override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonShortPress() override;
|
||||
void onButtonLongPress() override;
|
||||
void onRender() override;
|
||||
|
||||
void show(Tile *t); // Open the menu, onto a user tile
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override;
|
||||
|
||||
void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any
|
||||
void showPage(MenuPage page); // Load and display a MenuPage
|
||||
void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets
|
||||
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
|
||||
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
||||
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
|
||||
|
||||
MenuPage currentPage;
|
||||
uint8_t cursor = 0; // Which menu item is currently highlighted
|
||||
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
|
||||
|
||||
uint16_t systemInfoPanelHeight = 0; // Need to know before we render
|
||||
|
||||
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
|
||||
|
||||
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
|
||||
|
||||
WindowManager *windowManager = nullptr; // Convenient access to the InkHUD::WindowManager singleton
|
||||
Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,47 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
One item of a MenuPage, in InkHUD::MenuApplet
|
||||
|
||||
Added to MenuPages in InkHUD::showPage
|
||||
|
||||
- May open a submenu or exit
|
||||
- May perform an action
|
||||
- May toggle a bool value, shown by a checkbox
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./MenuAction.h"
|
||||
#include "./MenuPage.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
// One item of a MenuPage
|
||||
class MenuItem
|
||||
{
|
||||
public:
|
||||
std::string label;
|
||||
MenuAction action = NO_ACTION;
|
||||
MenuPage nextPage = EXIT;
|
||||
bool *checkState = nullptr;
|
||||
|
||||
// Various constructors, depending on the intended function of the item
|
||||
|
||||
MenuItem(const char *label, MenuPage nextPage) : label(label), nextPage(nextPage) {}
|
||||
MenuItem(const char *label, MenuAction action) : label(label), action(action) {}
|
||||
MenuItem(const char *label, MenuAction action, MenuPage nextPage) : label(label), action(action), nextPage(nextPage) {}
|
||||
MenuItem(const char *label, MenuAction action, MenuPage nextPage, bool *checkState)
|
||||
: label(label), action(action), nextPage(nextPage), checkState(checkState)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,30 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Sub-menu for InkHUD::MenuApplet
|
||||
Structure of the menu is defined in InkHUD::showPage
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
// Sub-menu for MenuApplet
|
||||
enum MenuPage : uint8_t {
|
||||
ROOT, // Initial menu page
|
||||
SEND,
|
||||
OPTIONS,
|
||||
APPLETS,
|
||||
AUTOSHOW,
|
||||
RECENTS, // Select length of "recentlyActiveSeconds"
|
||||
EXIT, // Dismiss the menu applet
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,40 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
A notification which might be displayed by the NotificationApplet
|
||||
|
||||
An instance of this class is offered to Applets via Applet::approveNotification, in case they want to veto the notification.
|
||||
An Applet should veto a notification if it is already displaying the same info which the notification would convey.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class Notification
|
||||
{
|
||||
public:
|
||||
enum Type : uint8_t { NOTIFICATION_MESSAGE_BROADCAST, NOTIFICATION_MESSAGE_DIRECT, NOTIFICATION_BATTERY } type;
|
||||
|
||||
uint32_t timestamp;
|
||||
|
||||
uint8_t getChannel() { return channel; }
|
||||
uint32_t getSender() { return sender; }
|
||||
uint8_t getBatteryPercentage() { return batteryPercentage; }
|
||||
|
||||
friend class NotificationApplet;
|
||||
|
||||
protected:
|
||||
uint8_t channel;
|
||||
uint32_t sender;
|
||||
uint8_t batteryPercentage;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,219 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./NotificationApplet.h"
|
||||
|
||||
#include "./Notification.h"
|
||||
|
||||
#include "RTC.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::NotificationApplet::onActivate()
|
||||
{
|
||||
textMessageObserver.observe(textMessageModule);
|
||||
}
|
||||
|
||||
// Note: This applet probably won't ever be deactivated
|
||||
void InkHUD::NotificationApplet::onDeactivate()
|
||||
{
|
||||
textMessageObserver.unobserve(textMessageModule);
|
||||
}
|
||||
|
||||
// Collect meta-info about the text message, and ask for approval for the notification
|
||||
// No need to save the message itself; we can use the cached InkHUD::latestMessage data during render()
|
||||
int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
// System applets are always active
|
||||
assert(isActive());
|
||||
|
||||
// Abort if feature disabled
|
||||
// This is a bit clumsy, but avoids complicated handling when the feature is enabled / disabled
|
||||
if (!settings.optionalFeatures.notifications)
|
||||
return 0;
|
||||
|
||||
// Abort if this is an outgoing message
|
||||
if (getFrom(p) == nodeDB->getNodeNum())
|
||||
return 0;
|
||||
|
||||
// Abort if message was only an "emoji reaction"
|
||||
// Possibly some implemetation of this in future?
|
||||
if (p->decoded.emoji)
|
||||
return 0;
|
||||
|
||||
Notification n;
|
||||
n.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
||||
|
||||
// Gather info: in-channel message
|
||||
if (isBroadcast(p->to)) {
|
||||
n.type = Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
|
||||
n.channel = p->channel;
|
||||
}
|
||||
|
||||
// Gather info: DM
|
||||
else {
|
||||
n.type = Notification::Type::NOTIFICATION_MESSAGE_DIRECT;
|
||||
n.sender = p->from;
|
||||
}
|
||||
|
||||
// Check if we should display the notification
|
||||
// A foreground applet might already be displaying this info
|
||||
hasNotification = true;
|
||||
currentNotification = n;
|
||||
if (isApproved()) {
|
||||
bringToForeground();
|
||||
WindowManager::getInstance()->forceUpdate();
|
||||
} else
|
||||
hasNotification = false; // Clear the pending notification: it was rejected
|
||||
|
||||
// Return zero: no issues here, carry on notifying other observers!
|
||||
return 0;
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onRender()
|
||||
{
|
||||
// Clear the region beneath the tile
|
||||
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
||||
// We do need to do this with the battery though, as it is an "overlay"
|
||||
fillRect(0, 0, width(), height(), WHITE);
|
||||
|
||||
setFont(fontSmall);
|
||||
|
||||
// Padding (horizontal)
|
||||
const uint16_t padW = 4;
|
||||
|
||||
// Main border
|
||||
drawRect(0, 0, width(), height(), BLACK);
|
||||
// drawRect(1, 1, width() - 2, height() - 2, BLACK);
|
||||
|
||||
// Timestamp (potentially)
|
||||
// ====================
|
||||
std::string ts = getTimeString(currentNotification.timestamp);
|
||||
uint16_t tsW = 0;
|
||||
int16_t divX = 0;
|
||||
|
||||
// Timestamp available
|
||||
if (ts.length() > 0) {
|
||||
tsW = getTextWidth(ts);
|
||||
divX = padW + tsW + padW;
|
||||
|
||||
hatchRegion(0, 0, divX, height(), 2, BLACK); // Fill with a dark background
|
||||
drawLine(divX, 0, divX, height() - 1, BLACK); // Draw divider between timestamp and main text
|
||||
|
||||
setCrop(1, 1, divX - 1, height() - 2);
|
||||
|
||||
// Drop shadow
|
||||
setTextColor(WHITE);
|
||||
printThick(padW + (tsW / 2), height() / 2, ts, 4, 4);
|
||||
|
||||
// Bold text
|
||||
setTextColor(BLACK);
|
||||
printThick(padW + (tsW / 2), height() / 2, ts, 2, 1);
|
||||
}
|
||||
|
||||
// Main text
|
||||
// =====================
|
||||
|
||||
// Background fill
|
||||
// - medium dark (1/3)
|
||||
hatchRegion(divX, 0, width() - divX - 1, height(), 3, BLACK);
|
||||
|
||||
uint16_t availableWidth = width() - divX - padW;
|
||||
std::string text = getNotificationText(availableWidth);
|
||||
|
||||
int16_t textM = divX + padW + (getTextWidth(text) / 2);
|
||||
|
||||
// Restrict area for printing
|
||||
// - don't overlap border, or diveder
|
||||
setCrop(divX + 1, 1, (width() - (divX + 1) - 1), height() - 2);
|
||||
|
||||
// Drop shadow
|
||||
// - thick white text
|
||||
setTextColor(WHITE);
|
||||
printThick(textM, height() / 2, text, 4, 4);
|
||||
|
||||
// Main text
|
||||
// - faux bold: double width
|
||||
setTextColor(BLACK);
|
||||
printThick(textM, height() / 2, text, 2, 1);
|
||||
}
|
||||
|
||||
// Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification
|
||||
// Called internally when we first get a "notifiable event", and then again before render,
|
||||
// in case autoshow swapped which applet was displayed
|
||||
bool InkHUD::NotificationApplet::isApproved()
|
||||
{
|
||||
// Instead of an assert
|
||||
if (!hasNotification) {
|
||||
LOG_WARN("No notif to approve");
|
||||
return false;
|
||||
}
|
||||
|
||||
return WindowManager::getInstance()->approveNotification(currentNotification);
|
||||
}
|
||||
|
||||
// Mark that the notification should no-longer be rendered
|
||||
// In addition to calling thing method, code needs to request a re-render of all applets
|
||||
void InkHUD::NotificationApplet::dismiss()
|
||||
{
|
||||
sendToBackground();
|
||||
hasNotification = false;
|
||||
// Not requesting update directly from this method,
|
||||
// as it is used to dismiss notifications which have been made redundant by autoshow settings, before they are ever drawn
|
||||
}
|
||||
|
||||
// Get a string for the main body text of a notification
|
||||
// Formatted to suit screen width
|
||||
// Takes info from InkHUD::currentNotification
|
||||
std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvailable)
|
||||
{
|
||||
assert(hasNotification);
|
||||
|
||||
std::string text;
|
||||
|
||||
// Text message
|
||||
// ==============
|
||||
|
||||
if (IS_ONE_OF(currentNotification.type, Notification::Type::NOTIFICATION_MESSAGE_DIRECT,
|
||||
Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) {
|
||||
|
||||
// Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently
|
||||
bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
|
||||
|
||||
// Pick source of message
|
||||
MessageStore::Message *message = isBroadcast ? &latestMessage.broadcast : &latestMessage.dm;
|
||||
|
||||
// Find info about the sender
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender);
|
||||
|
||||
// Leading tag (channel vs. DM)
|
||||
text += isBroadcast ? "From:" : "DM: ";
|
||||
|
||||
// Sender id
|
||||
if (node && node->has_user)
|
||||
text += node->user.short_name;
|
||||
else
|
||||
text += hexifyNodeNum(message->sender);
|
||||
|
||||
// Check if text fits
|
||||
// - use a longer string, if we have the space
|
||||
if (getTextWidth(text) < widthAvailable * 0.5) {
|
||||
text.clear();
|
||||
|
||||
// Leading tag (channel vs. DM)
|
||||
text += isBroadcast ? "Msg from " : "DM from ";
|
||||
|
||||
// Sender id
|
||||
if (node && node->has_user)
|
||||
text += node->user.short_name;
|
||||
else
|
||||
text += hexifyNodeNum(message->sender);
|
||||
|
||||
text += ": ";
|
||||
text += message->text;
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,49 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Pop-up notification bar, on screen top edge
|
||||
Displays information we feel is important, but which is not shown on currently focussed applet(s)
|
||||
E.g.: messages, while viewing map, etc
|
||||
|
||||
Feature should be optional; enable disable via on-screen menu
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "concurrency/OSThread.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class NotificationApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
|
||||
|
||||
bool isApproved(); // Does a foreground applet make notification redundant?
|
||||
void dismiss(); // Close the Notification Popup
|
||||
|
||||
protected:
|
||||
// Get notified when a new text message arrives
|
||||
CallbackObserver<NotificationApplet, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<NotificationApplet, const meshtastic_MeshPacket *>(this, &NotificationApplet::onReceiveTextMessage);
|
||||
|
||||
std::string getNotificationText(uint16_t widthAvailable); // Get text for notification, to suit screen width
|
||||
|
||||
bool hasNotification = false; // Only used for assert. Todo: remove?
|
||||
Notification currentNotification; // Set when something notification-worthy happens. Used by render()
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,96 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./PairingApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::PairingApplet::PairingApplet()
|
||||
{
|
||||
// Grab the window manager singleton, for convenience
|
||||
windowManager = WindowManager::getInstance();
|
||||
}
|
||||
|
||||
void InkHUD::PairingApplet::onRender()
|
||||
{
|
||||
// Header
|
||||
setFont(fontLarge);
|
||||
printAt(X(0.5), Y(0.25), "Bluetooth", CENTER, BOTTOM);
|
||||
setFont(fontSmall);
|
||||
printAt(X(0.5), Y(0.25), "Enter this code", CENTER, TOP);
|
||||
|
||||
// Passkey
|
||||
setFont(fontLarge);
|
||||
printThick(X(0.5), Y(0.5), passkey.substr(0, 3) + " " + passkey.substr(3), 3, 2);
|
||||
|
||||
// Device's bluetooth name, if it will fit
|
||||
setFont(fontSmall);
|
||||
std::string name = "Name: " + std::string(getDeviceName());
|
||||
if (getTextWidth(name) > width()) // Too wide, try without the leading "Name: "
|
||||
name = std::string(getDeviceName());
|
||||
if (getTextWidth(name) < width()) // Does it fit?
|
||||
printAt(X(0.5), Y(0.75), name, CENTER, MIDDLE);
|
||||
}
|
||||
|
||||
void InkHUD::PairingApplet::onActivate()
|
||||
{
|
||||
bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus);
|
||||
}
|
||||
|
||||
void InkHUD::PairingApplet::onDeactivate()
|
||||
{
|
||||
bluetoothStatusObserver.unobserve(&bluetoothStatus->onNewStatus);
|
||||
}
|
||||
|
||||
void InkHUD::PairingApplet::onForeground()
|
||||
{
|
||||
// If another applet has locked the display, ask it to exit
|
||||
Applet *other = windowManager->whoLocked();
|
||||
if (other != nullptr)
|
||||
other->sendToBackground();
|
||||
|
||||
windowManager->claimFullscreen(this); // Take ownership of the fullscreen tile
|
||||
windowManager->lock(this); // Prevent user applets from requesting update
|
||||
}
|
||||
void InkHUD::PairingApplet::onBackground()
|
||||
{
|
||||
windowManager->releaseFullscreen(); // Relinquish ownership of the fullscreen tile
|
||||
windowManager->unlock(this); // Allow normal user applet update requests to resume
|
||||
|
||||
// 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
|
||||
windowManager->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status)
|
||||
{
|
||||
// The standard Meshtastic convention is to pass these "generic" Status objects,
|
||||
// check their type, and then cast them.
|
||||
// We'll mimic that behavior, just to keep in line with the other Statuses,
|
||||
// even though I'm not sure what the original reason for jumping through these extra hoops was.
|
||||
assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH);
|
||||
meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)status;
|
||||
|
||||
// When pairing begins
|
||||
if (bluetoothStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) {
|
||||
// Store the passkey for rendering
|
||||
passkey = bluetoothStatus->getPasskey();
|
||||
|
||||
// Make sure no other system applets have a lock on the display
|
||||
// Boot screen, menu, etc
|
||||
Applet *lockOwner = windowManager->whoLocked();
|
||||
if (lockOwner)
|
||||
lockOwner->sendToBackground();
|
||||
|
||||
// Show pairing screen
|
||||
bringToForeground();
|
||||
}
|
||||
|
||||
// When pairing ends
|
||||
// or rather, when something changes, and we shouldn't be showing the pairing screen
|
||||
else if (isForeground())
|
||||
sendToBackground();
|
||||
|
||||
return 0; // No special result to report back to Observable
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,43 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Shows the Bluetooth passkey during pairing
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class PairingApplet : public Applet
|
||||
{
|
||||
public:
|
||||
PairingApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
|
||||
int onBluetoothStatusUpdate(const meshtastic::Status *status);
|
||||
|
||||
protected:
|
||||
// Get notified when status of the Bluetooth connection changes
|
||||
CallbackObserver<PairingApplet, const meshtastic::Status *> bluetoothStatusObserver =
|
||||
CallbackObserver<PairingApplet, const meshtastic::Status *>(this, &PairingApplet::onBluetoothStatusUpdate);
|
||||
|
||||
std::string passkey = ""; // Passkey. Six digits, possibly with leading zeros
|
||||
|
||||
WindowManager *windowManager = nullptr; // For convenience. Set in constructor.
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,21 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./PlaceholderApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::PlaceholderApplet::PlaceholderApplet()
|
||||
{
|
||||
// Because this applet sometimes gets processed as if it were a bonafide user applet,
|
||||
// it's probably better that we do give it a human readable name, just in case it comes up later.
|
||||
// For genuine user applets, this is set by WindowManager::addApplet
|
||||
Applet::name = "Placeholder";
|
||||
}
|
||||
|
||||
void InkHUD::PlaceholderApplet::onRender()
|
||||
{
|
||||
// This placeholder applet fills its area with sparse diagonal lines
|
||||
hatchRegion(0, 0, width(), height(), 8, BLACK);
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,30 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Shown when a tile doesn't have any other valid Applets
|
||||
Fills the area with diagonal lines
|
||||
|
||||
*/
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class PlaceholderApplet : public Applet
|
||||
{
|
||||
public:
|
||||
PlaceholderApplet();
|
||||
void onRender() override;
|
||||
|
||||
// Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet.
|
||||
// The window manager decides when and where it should be rendered
|
||||
// It may be drawn to several different tiles during on WindowManager::render call
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,234 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./TipsApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::TipsApplet::TipsApplet()
|
||||
{
|
||||
// Grab the window manager singleton, for convenience
|
||||
windowManager = WindowManager::getInstance();
|
||||
}
|
||||
|
||||
void InkHUD::TipsApplet::onRender()
|
||||
{
|
||||
switch (tipQueue.front()) {
|
||||
case Tip::WELCOME:
|
||||
renderWelcome();
|
||||
break;
|
||||
|
||||
case Tip::FINISH_SETUP: {
|
||||
setFont(fontLarge);
|
||||
printAt(0, 0, "Tip: Finish Setup");
|
||||
|
||||
setFont(fontSmall);
|
||||
int16_t cursorY = fontLarge.lineHeight() * 1.5;
|
||||
printAt(0, cursorY, "- connect antenna");
|
||||
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- connect a client app");
|
||||
|
||||
// Only if region not set
|
||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- set region");
|
||||
}
|
||||
|
||||
// Only if tz not set
|
||||
if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) {
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- set timezone");
|
||||
}
|
||||
|
||||
cursorY += fontSmall.lineHeight() * 1.5;
|
||||
printAt(0, cursorY, "More info at meshtastic.org");
|
||||
|
||||
setFont(fontSmall);
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
} break;
|
||||
|
||||
case Tip::SAFE_SHUTDOWN: {
|
||||
setFont(fontLarge);
|
||||
printAt(0, 0, "Tip: Shutdown");
|
||||
|
||||
setFont(fontSmall);
|
||||
std::string shutdown;
|
||||
shutdown += "Before removing power, please shutdown from InkHUD menu, or a client app. \n";
|
||||
shutdown += "\n";
|
||||
shutdown += "This ensures data is saved.";
|
||||
printWrapped(0, fontLarge.lineHeight() * 1.5, width(), shutdown);
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
|
||||
} break;
|
||||
|
||||
case Tip::CUSTOMIZATION: {
|
||||
setFont(fontLarge);
|
||||
printAt(0, 0, "Tip: Customization");
|
||||
|
||||
setFont(fontSmall);
|
||||
printWrapped(0, fontLarge.lineHeight() * 1.5, width(),
|
||||
"Configure & control display with the InkHUD menu. Optional features, layout, rotation, and more.");
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
} break;
|
||||
|
||||
case Tip::BUTTONS: {
|
||||
setFont(fontLarge);
|
||||
printAt(0, 0, "Tip: Buttons");
|
||||
|
||||
setFont(fontSmall);
|
||||
int16_t cursorY = fontLarge.lineHeight() * 1.5;
|
||||
|
||||
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");
|
||||
cursorY += fontSmall.lineHeight() * 1.5;
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
} break;
|
||||
|
||||
case Tip::ROTATION: {
|
||||
setFont(fontLarge);
|
||||
printAt(0, 0, "Tip: Rotation");
|
||||
|
||||
setFont(fontSmall);
|
||||
printWrapped(0, fontLarge.lineHeight() * 1.5, width(),
|
||||
"To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate.");
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
|
||||
// Revert the "flip screen" setting, preventing this message showing again
|
||||
config.display.flip_screen = false;
|
||||
nodeDB->saveToDisk(SEGMENT_DEVICESTATE);
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
// This tip has its own render method, only because it's a big block of code
|
||||
// Didn't want to clutter up the switch in onRender too much
|
||||
void InkHUD::TipsApplet::renderWelcome()
|
||||
{
|
||||
uint16_t padW = X(0.05);
|
||||
|
||||
// Block 1 - logo & title
|
||||
// ========================
|
||||
|
||||
// Logo size
|
||||
uint16_t logoWLimit = X(0.3);
|
||||
uint16_t logoHLimit = Y(0.3);
|
||||
uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit);
|
||||
uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit);
|
||||
|
||||
// Title size
|
||||
setFont(fontLarge);
|
||||
std::string title;
|
||||
if (width() >= 200) // Future proofing: hide if *tiny* display
|
||||
title = "meshtastic.org";
|
||||
uint16_t titleW = getTextWidth(title);
|
||||
|
||||
// Center the block
|
||||
// Desired effect: equal margin from display edge for logo left and title right
|
||||
int16_t block1Y = 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);
|
||||
|
||||
// Draw block
|
||||
drawLogo(logoCX, block1Y, logoW, logoH);
|
||||
printAt(titleCX, block1Y, title, CENTER, MIDDLE);
|
||||
|
||||
// Block 2 - subtitle
|
||||
// =======================
|
||||
setFont(fontSmall);
|
||||
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);
|
||||
|
||||
// Block 3 - press to continue
|
||||
// ============================
|
||||
printAt(X(0.5), Y(1), "Press button to continue", CENTER, BOTTOM);
|
||||
}
|
||||
|
||||
// Grab fullscreen tile, and lock the window manager, when applet is shown
|
||||
void InkHUD::TipsApplet::onForeground()
|
||||
{
|
||||
windowManager->lock(this);
|
||||
windowManager->claimFullscreen(this);
|
||||
}
|
||||
|
||||
void InkHUD::TipsApplet::onBackground()
|
||||
{
|
||||
windowManager->releaseFullscreen();
|
||||
windowManager->unlock(this);
|
||||
}
|
||||
|
||||
void InkHUD::TipsApplet::onActivate()
|
||||
{
|
||||
// Decide which tips (if any) should be shown to user after the boot screen
|
||||
|
||||
// Welcome screen
|
||||
if (settings.tips.firstBoot)
|
||||
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)
|
||||
tipQueue.push_back(Tip::FINISH_SETUP);
|
||||
|
||||
// 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 will be brought to foreground when boot screen closes, via TipsApplet::onLockAvailable
|
||||
}
|
||||
|
||||
// While our applet has the window manager locked, we will receive the button input
|
||||
void InkHUD::TipsApplet::onButtonShortPress()
|
||||
{
|
||||
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) {
|
||||
settings.tips.firstBoot = false;
|
||||
saveDataToFlash();
|
||||
}
|
||||
|
||||
// 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
|
||||
sendToBackground();
|
||||
windowManager->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
// More tips left
|
||||
else
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
// If the wm lock has just become availale (rendering, input), and we've still got tips, grab it!
|
||||
// This situation would arise if bluetooth pairing occurs while TipsApplet was already shown (after pairing)
|
||||
// Note: this event is only raised when *other* applets unlock the window manager
|
||||
void InkHUD::TipsApplet::onLockAvailable()
|
||||
{
|
||||
if (!tipQueue.empty())
|
||||
bringToForeground();
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,52 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Shows info on how to use InkHUD
|
||||
- tutorial at first boot
|
||||
- additional tips in certain situation (e.g. bad shutdown, region unset)
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class TipsApplet : public Applet
|
||||
{
|
||||
protected:
|
||||
enum class Tip {
|
||||
WELCOME,
|
||||
FINISH_SETUP,
|
||||
SAFE_SHUTDOWN,
|
||||
CUSTOMIZATION,
|
||||
BUTTONS,
|
||||
ROTATION,
|
||||
};
|
||||
|
||||
public:
|
||||
TipsApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onActivate() override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonShortPress() override;
|
||||
void onLockAvailable() override; // Reopen if interrupted by bluetooth pairing
|
||||
|
||||
protected:
|
||||
void renderWelcome(); // Very first screen of tutorial
|
||||
|
||||
std::deque<Tip> tipQueue; // List of tips to show, one after another
|
||||
|
||||
WindowManager *windowManager = nullptr; // For convenience. Set in constructor.
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,133 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./AllMessageApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::AllMessageApplet::onActivate()
|
||||
{
|
||||
textMessageObserver.observe(textMessageModule);
|
||||
}
|
||||
|
||||
void InkHUD::AllMessageApplet::onDeactivate()
|
||||
{
|
||||
textMessageObserver.unobserve(textMessageModule);
|
||||
}
|
||||
|
||||
// We're not consuming the data passed to this method;
|
||||
// we're just just using it to trigger a render
|
||||
int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
// Abort if applet fully deactivated
|
||||
// Already handled by onActivate and onDeactivate, but good practice for all applets
|
||||
if (!isActive())
|
||||
return 0;
|
||||
|
||||
// Abort if this is an outgoing message
|
||||
if (getFrom(p) == nodeDB->getNodeNum())
|
||||
return 0;
|
||||
|
||||
// Abort if message was only an "emoji reaction"
|
||||
// Possibly some implemetation of this in future?
|
||||
if (p->decoded.emoji)
|
||||
return 0;
|
||||
|
||||
requestAutoshow(); // Want to become foreground, if permitted
|
||||
requestUpdate(); // Want to update display, if applet is foreground
|
||||
|
||||
// Return zero: no issues here, carry on notifying other observers!
|
||||
return 0;
|
||||
}
|
||||
|
||||
void InkHUD::AllMessageApplet::onRender()
|
||||
{
|
||||
setFont(fontSmall);
|
||||
|
||||
// Find newest message, regardless of whether DM or broadcast
|
||||
MessageStore::Message *message;
|
||||
if (latestMessage.wasBroadcast)
|
||||
message = &latestMessage.broadcast;
|
||||
else
|
||||
message = &latestMessage.dm;
|
||||
|
||||
// Short circuit: no text message
|
||||
if (!message->sender) {
|
||||
printAt(X(0.5), Y(0.5), "No Message", CENTER, MIDDLE);
|
||||
return;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Header (sender, timestamp)
|
||||
// ===========================
|
||||
|
||||
// Y position for divider
|
||||
// - between header text and messages
|
||||
|
||||
std::string header;
|
||||
|
||||
// RX Time
|
||||
// - if valid
|
||||
std::string timeString = getTimeString(message->timestamp);
|
||||
if (timeString.length() > 0) {
|
||||
header += timeString;
|
||||
header += ": ";
|
||||
}
|
||||
|
||||
// Sender's id
|
||||
// - shortname, if available, or
|
||||
// - node id
|
||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender);
|
||||
if (sender && sender->has_user) {
|
||||
header += sender->user.short_name;
|
||||
header += " (";
|
||||
header += sender->user.long_name;
|
||||
header += ")";
|
||||
} else
|
||||
header += hexifyNodeNum(message->sender);
|
||||
|
||||
// Draw a "standard" applet header
|
||||
drawHeader(header);
|
||||
|
||||
// Fade the right edge of the header, if text spills over edge
|
||||
uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect
|
||||
uint8_t hF = getHeaderHeight(); // Height of fade effect
|
||||
if (getCursorX() > width())
|
||||
hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE);
|
||||
|
||||
// Dimensions of the header
|
||||
constexpr int16_t padDivH = 2;
|
||||
const int16_t headerDivY = Applet::getHeaderHeight() - 1;
|
||||
|
||||
// ===================
|
||||
// Print message text
|
||||
// ===================
|
||||
|
||||
// Extra gap below the header
|
||||
int16_t textTop = headerDivY + padDivH;
|
||||
|
||||
// Determine size if printed large
|
||||
setFont(fontLarge);
|
||||
uint32_t textHeight = getWrappedTextHeight(0, width(), message->text);
|
||||
|
||||
// If too large, swap to small font
|
||||
if (textHeight + textTop > (uint32_t)height()) // (compare signed and unsigned)
|
||||
setFont(fontSmall);
|
||||
|
||||
// Print text
|
||||
printWrapped(0, textTop, width(), message->text);
|
||||
}
|
||||
|
||||
// Don't show notifications for text messages when our applet is displayed
|
||||
bool InkHUD::AllMessageApplet::approveNotification(Notification &n)
|
||||
{
|
||||
if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)
|
||||
return false;
|
||||
|
||||
else if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT)
|
||||
return false;
|
||||
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,49 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Shows the latest incoming text message, as well as sender.
|
||||
Both broadcast and direct messages will be shown here, from all channels.
|
||||
|
||||
This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages
|
||||
This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message.
|
||||
This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage
|
||||
|
||||
We do still receive notifications from the text message module though,
|
||||
to know when a new message has arrived, and trigger the update.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
#include "modules/TextMessageModule.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class Applet;
|
||||
|
||||
class AllMessageApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
|
||||
|
||||
bool approveNotification(Notification &n) override; // Which notifications to suppress
|
||||
|
||||
protected:
|
||||
// Used to register our text message callback
|
||||
CallbackObserver<AllMessageApplet, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<AllMessageApplet, const meshtastic_MeshPacket *>(this, &AllMessageApplet::onReceiveTextMessage);
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,126 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./DMApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::DMApplet::onActivate()
|
||||
{
|
||||
textMessageObserver.observe(textMessageModule);
|
||||
}
|
||||
|
||||
void InkHUD::DMApplet::onDeactivate()
|
||||
{
|
||||
textMessageObserver.unobserve(textMessageModule);
|
||||
}
|
||||
|
||||
// We're not consuming the data passed to this method;
|
||||
// we're just just using it to trigger a render
|
||||
int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
// Abort if applet fully deactivated
|
||||
// Already handled by onActivate and onDeactivate, but good practice for all applets
|
||||
if (!isActive())
|
||||
return 0;
|
||||
|
||||
// Abort if only an "emoji reactions"
|
||||
// Possibly some implemetation of this in future?
|
||||
if (p->decoded.emoji)
|
||||
return 0;
|
||||
|
||||
// If DM (not broadcast)
|
||||
if (!isBroadcast(p->to)) {
|
||||
// Want to update display, if applet is foreground
|
||||
requestUpdate();
|
||||
|
||||
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
|
||||
if (getFrom(p) != nodeDB->getNodeNum())
|
||||
requestAutoshow();
|
||||
}
|
||||
|
||||
// Return zero: no issues here, carry on notifying other observers!
|
||||
return 0;
|
||||
}
|
||||
|
||||
void InkHUD::DMApplet::onRender()
|
||||
{
|
||||
setFont(fontSmall);
|
||||
|
||||
// Abort if no text message
|
||||
if (!latestMessage.dm.sender) {
|
||||
printAt(X(0.5), Y(0.5), "No DMs", CENTER, MIDDLE);
|
||||
return;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Header (sender, timestamp)
|
||||
// ===========================
|
||||
|
||||
// Y position for divider
|
||||
// - between header text and messages
|
||||
|
||||
std::string header;
|
||||
|
||||
// RX Time
|
||||
// - if valid
|
||||
std::string timeString = getTimeString(latestMessage.dm.timestamp);
|
||||
if (timeString.length() > 0) {
|
||||
header += timeString;
|
||||
header += ": ";
|
||||
}
|
||||
|
||||
// Sender's id
|
||||
// - shortname, if available, or
|
||||
// - node id
|
||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage.dm.sender);
|
||||
if (sender && sender->has_user) {
|
||||
header += sender->user.short_name;
|
||||
header += " (";
|
||||
header += sender->user.long_name;
|
||||
header += ")";
|
||||
} else
|
||||
header += hexifyNodeNum(latestMessage.dm.sender);
|
||||
|
||||
// Draw a "standard" applet header
|
||||
drawHeader(header);
|
||||
|
||||
// Fade the right edge of the header, if text spills over edge
|
||||
uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect
|
||||
uint8_t hF = getHeaderHeight(); // Height of fade effect
|
||||
if (getCursorX() > width())
|
||||
hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE);
|
||||
|
||||
// Dimensions of the header
|
||||
constexpr int16_t padDivH = 2;
|
||||
const int16_t headerDivY = Applet::getHeaderHeight() - 1;
|
||||
|
||||
// ===================
|
||||
// Print message text
|
||||
// ===================
|
||||
|
||||
// Extra gap below the header
|
||||
int16_t textTop = headerDivY + padDivH;
|
||||
|
||||
// Determine size if printed large
|
||||
setFont(fontLarge);
|
||||
uint32_t textHeight = getWrappedTextHeight(0, width(), latestMessage.dm.text);
|
||||
|
||||
// If too large, swap to small font
|
||||
if (textHeight + textTop > (uint32_t)height()) // (compare signed and unsigned)
|
||||
setFont(fontSmall);
|
||||
|
||||
// Print text
|
||||
printWrapped(0, textTop, width(), latestMessage.dm.text);
|
||||
}
|
||||
|
||||
// Don't show notifications for direct messages when our applet is displayed
|
||||
bool InkHUD::DMApplet::approveNotification(Notification &n)
|
||||
{
|
||||
if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT)
|
||||
return false;
|
||||
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,49 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Shows the latest incoming *Direct Message* (DM), as well as sender.
|
||||
This compliments the threaded message applets
|
||||
|
||||
This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages
|
||||
This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message.
|
||||
This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage
|
||||
|
||||
We do still receive notifications from the text message module though,
|
||||
to know when a new message has arrived, and trigger the update.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
#include "modules/TextMessageModule.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class Applet;
|
||||
|
||||
class DMApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
|
||||
|
||||
bool approveNotification(Notification &n) override; // Which notifications to suppress
|
||||
|
||||
protected:
|
||||
// Used to register our text message callback
|
||||
CallbackObserver<DMApplet, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<DMApplet, const meshtastic_MeshPacket *>(this, &DMApplet::onReceiveTextMessage);
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,123 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "RTC.h"
|
||||
|
||||
#include "gps/GeoCoord.h"
|
||||
|
||||
#include "./HeardApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::HeardApplet::onActivate()
|
||||
{
|
||||
// When applet begins, pre-fill with stale info from NodeDB
|
||||
populateFromNodeDB();
|
||||
}
|
||||
|
||||
void InkHUD::HeardApplet::onDeactivate()
|
||||
{
|
||||
// Avoid an unlikely situation where frquent activation / deactivation populated duplicate info from node DB
|
||||
cards.clear();
|
||||
}
|
||||
|
||||
// When base applet hears a new packet, it extracts the info and passes it to us as CardInfo
|
||||
// We need to store it (at front to sort recent), and request display update if our list has visibly changed as a result
|
||||
void InkHUD::HeardApplet::handleParsed(CardInfo c)
|
||||
{
|
||||
// Grab the previous entry.
|
||||
// To check if the new data is different enough to justify re-render
|
||||
// Need to cache now, before we manipulate the deque
|
||||
CardInfo previous;
|
||||
if (!cards.empty())
|
||||
previous = cards.at(0);
|
||||
|
||||
// If we're updating an existing entry, remove the old one. Will reinsert at front
|
||||
for (auto it = cards.begin(); it != cards.end(); ++it) {
|
||||
if (it->nodeNum == c.nodeNum) {
|
||||
cards.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cards.push_front(c); // Insert into base class' card collection
|
||||
cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen
|
||||
|
||||
// Our rendered image needs to change if:
|
||||
if (previous.nodeNum != c.nodeNum // Different node
|
||||
|| previous.signal != c.signal // or different signal strength
|
||||
|| previous.distanceMeters != c.distanceMeters // or different position
|
||||
|| previous.hopsAway != c.hopsAway) // or different hops away
|
||||
{
|
||||
requestAutoshow();
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
// When applet is activated, pre-fill with stale data from NodeDB
|
||||
// We're sorting using the last_heard value. Succeptible to weirdness if node's RTC changes.
|
||||
// No SNR is available in node db, so we can't calculate signal either
|
||||
// These initial cards from node db will be gradually pushed out by new packets which originate from out base applet instead
|
||||
void InkHUD::HeardApplet::populateFromNodeDB()
|
||||
{
|
||||
// Fill a collection with pointers to each node in db
|
||||
std::vector<meshtastic_NodeInfoLite *> ordered;
|
||||
for (auto mn = nodeDB->meshNodes->begin(); mn != nodeDB->meshNodes->end(); ++mn) {
|
||||
// Only copy if valid, and not our own node
|
||||
if (mn->num != 0 && mn->num != nodeDB->getNodeNum())
|
||||
ordered.push_back(&*mn);
|
||||
}
|
||||
|
||||
// Sort the collection by age
|
||||
std::sort(ordered.begin(), ordered.end(), [](meshtastic_NodeInfoLite *top, meshtastic_NodeInfoLite *bottom) -> bool {
|
||||
return (top->last_heard > bottom->last_heard);
|
||||
});
|
||||
|
||||
// Keep the most recent entries onlyt
|
||||
// Just enough to fill the screen
|
||||
if (ordered.size() > maxCards())
|
||||
ordered.resize(maxCards());
|
||||
|
||||
// Create card info for these (stale) node observations
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
for (meshtastic_NodeInfoLite *node : ordered) {
|
||||
CardInfo c;
|
||||
c.nodeNum = node->num;
|
||||
|
||||
if (node->has_hops_away)
|
||||
c.hopsAway = node->hops_away;
|
||||
|
||||
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
|
||||
// Get lat and long as float
|
||||
// Meshtastic stores these as integers internally
|
||||
float ourLat = ourNode->position.latitude_i * 1e-7;
|
||||
float ourLong = ourNode->position.longitude_i * 1e-7;
|
||||
float theirLat = node->position.latitude_i * 1e-7;
|
||||
float theirLong = node->position.longitude_i * 1e-7;
|
||||
|
||||
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
|
||||
}
|
||||
|
||||
// Insert into the card collection (member of base class)
|
||||
cards.push_back(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Text drawn in the usual applet header
|
||||
// Handled by base class: ChronoListApplet
|
||||
std::string InkHUD::HeardApplet::getHeaderText()
|
||||
{
|
||||
uint16_t nodeCount = nodeDB->getNumMeshNodes() - 1; // Don't count our own node
|
||||
|
||||
std::string text = "Heard: ";
|
||||
|
||||
// Print node count, if nodeDB not yet nearing full
|
||||
if (nodeCount < MAX_NUM_NODES) {
|
||||
text += to_string(nodeCount); // Max nodes
|
||||
text += " ";
|
||||
text += (nodeCount == 1) ? "node" : "nodes";
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,35 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Shows a list of all nodes (recently heard or not), sorted by time last heard.
|
||||
Most of the work is done by the InkHUD::NodeListApplet base class
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class HeardApplet : public NodeListApplet
|
||||
{
|
||||
public:
|
||||
HeardApplet() : NodeListApplet("HeardApplet") {}
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
|
||||
protected:
|
||||
void handleParsed(CardInfo c) override; // Store new info, and update display if needed
|
||||
std::string getHeaderText() override; // Set title for this applet
|
||||
|
||||
void populateFromNodeDB(); // Pre-fill the CardInfo collection from NodeDB
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,110 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./PositionsApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::PositionsApplet::onRender()
|
||||
{
|
||||
// Draw the usual map applet first
|
||||
MapApplet::onRender();
|
||||
|
||||
// Draw our latest "node of interest" as a special marker
|
||||
// -------------------------------------------------------
|
||||
// We might be rendering because we got a position packet from them
|
||||
// We might be rendering because our own position updated
|
||||
// Either way, we still highlight which node most recently sent us a position packet
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom);
|
||||
if (node && nodeDB->hasValidPosition(node) && enoughMarkers())
|
||||
drawLabeledMarker(node);
|
||||
}
|
||||
|
||||
// Determine if we need to redraw the map, when we receive a new position packet
|
||||
ProcessMessage InkHUD::PositionsApplet::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
// If applet is not active, we shouldn't be handling any data
|
||||
// It's good practice for all applets to implement an early return like this
|
||||
// for PositionsApplet, this is **required** - it's where we're handling active vs deactive
|
||||
if (!isActive())
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Try decode a position from the packet
|
||||
bool hasPosition = false;
|
||||
float lat;
|
||||
float lng;
|
||||
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) {
|
||||
meshtastic_Position position = meshtastic_Position_init_default;
|
||||
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) {
|
||||
if (position.has_latitude_i && position.has_longitude_i // Actually has position
|
||||
&& (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island"
|
||||
{
|
||||
hasPosition = true;
|
||||
lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format
|
||||
lng = position.longitude_i * 1e-7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if we didn't get a valid position
|
||||
if (!hasPosition)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
bool hasHopsAway = (mp.hop_start != 0 && mp.hop_limit <= mp.hop_start); // From NodeDB::updateFrom
|
||||
uint8_t hopsAway = mp.hop_start - mp.hop_limit;
|
||||
|
||||
// Determine if the position packet would change anything on-screen
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
bool somethingChanged = false;
|
||||
|
||||
// If our own position
|
||||
if (isFromUs(&mp)) {
|
||||
// We get frequent position updates from connected phone
|
||||
// Only update if we're travelled some distance, for rate limiting
|
||||
// Todo: smarter detection of position changes
|
||||
if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) {
|
||||
somethingChanged = true;
|
||||
ourLastLat = lat;
|
||||
ourLastLng = lng;
|
||||
}
|
||||
}
|
||||
|
||||
// If someone else's position
|
||||
else {
|
||||
// Check if this position is from someone different than our previous position packet
|
||||
if (mp.from != lastFrom) {
|
||||
somethingChanged = true;
|
||||
lastFrom = mp.from;
|
||||
lastLat = lat;
|
||||
lastLng = lng;
|
||||
lastHopsAway = hopsAway;
|
||||
}
|
||||
|
||||
// Same sender: check if position changed
|
||||
// Todo: smarter detection of position changes
|
||||
else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) {
|
||||
somethingChanged = true;
|
||||
lastLat = lat;
|
||||
lastLng = lng;
|
||||
}
|
||||
|
||||
// Same sender, same position: check if hops changed
|
||||
// Only pay attention if the hopsAway value is valid
|
||||
else if (hasHopsAway && (hopsAway != lastHopsAway)) {
|
||||
somethingChanged = true;
|
||||
lastHopsAway = hopsAway;
|
||||
}
|
||||
}
|
||||
|
||||
// Decision reached
|
||||
// -----------------
|
||||
|
||||
if (somethingChanged) {
|
||||
requestAutoshow(); // Todo: only request this in some situations?
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,43 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Plots position of all nodes from DB, with North facing up.
|
||||
Scaled to fit the most distant node.
|
||||
Size of cross represents hops away.
|
||||
The node which has most recently sent a position will be labeled.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h"
|
||||
|
||||
#include "SinglePortModule.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class PositionsApplet : public MapApplet, public SinglePortModule
|
||||
{
|
||||
public:
|
||||
PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {}
|
||||
void onRender() override;
|
||||
|
||||
protected:
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
NodeNum lastFrom; // Sender of most recent (non-local) position packet
|
||||
float lastLat;
|
||||
float lastLng;
|
||||
float lastHopsAway;
|
||||
|
||||
float ourLastLat; // Info about the most recent (non-local) position packet
|
||||
float ourLastLng; // Info about most recent *local* position
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,150 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./RecentsListApplet.h"
|
||||
|
||||
#include "RTC.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::RecentsListApplet::RecentsListApplet() : NodeListApplet("RecentsListApplet"), concurrency::OSThread("RecentsListApplet")
|
||||
{
|
||||
// No scheduled tasks initially
|
||||
OSThread::disable();
|
||||
}
|
||||
|
||||
void InkHUD::RecentsListApplet::onActivate()
|
||||
{
|
||||
// When the applet is activated, begin scheduled purging of any nodes which are no longer "active"
|
||||
OSThread::enabled = true;
|
||||
OSThread::setIntervalFromNow(60 * 1000UL); // Every minute
|
||||
}
|
||||
|
||||
void InkHUD::RecentsListApplet::onDeactivate()
|
||||
{
|
||||
// Halt scheduled purging
|
||||
OSThread::disable();
|
||||
}
|
||||
|
||||
int32_t InkHUD::RecentsListApplet::runOnce()
|
||||
{
|
||||
prune(); // Remove CardInfo and Age record for nodes which we haven't heard recently
|
||||
return OSThread::interval;
|
||||
}
|
||||
|
||||
// When base applet hears a new packet, it extracts the info and passes it to us as CardInfo
|
||||
// We need to store it (at front to sort recent), and request display update if our list has visibly changed as a result
|
||||
// We also need to record the current time against the nodenum, so we know when it becomes inactive
|
||||
void InkHUD::RecentsListApplet::handleParsed(CardInfo c)
|
||||
{
|
||||
// Grab the previous entry.
|
||||
// To check if the new data is different enough to justify re-render
|
||||
// Need to cache now, before we manipulate the deque
|
||||
CardInfo previous;
|
||||
if (!cards.empty())
|
||||
previous = cards.at(0);
|
||||
|
||||
// If we're updating an existing entry, remove the old one. Will reinsert at front
|
||||
for (auto it = cards.begin(); it != cards.end(); ++it) {
|
||||
if (it->nodeNum == c.nodeNum) {
|
||||
cards.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cards.push_front(c); // Store this CardInfo
|
||||
cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen
|
||||
|
||||
// Record the time of this observation
|
||||
// Used to count active nodes, and to know when to prune inactive nodes
|
||||
seenNow(c.nodeNum);
|
||||
|
||||
// Our rendered image needs to change if:
|
||||
if (previous.nodeNum != c.nodeNum // Different node
|
||||
|| previous.signal != c.signal // or different signal strength
|
||||
|| previous.distanceMeters != c.distanceMeters // or different position
|
||||
|| previous.hopsAway != c.hopsAway) // or different hops away
|
||||
{
|
||||
prune(); // Take the opportunity now to remove inactive nodes
|
||||
requestAutoshow();
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
// Record the time (millis, right now) that we hear a node
|
||||
// If we do not hear from a node for a while, its card and age info will be removed by the purge method, which runs regularly
|
||||
void InkHUD::RecentsListApplet::seenNow(NodeNum nodeNum)
|
||||
{
|
||||
// If we're updating an existing entry, remove the old one. Will reinsert at front
|
||||
for (auto it = ages.begin(); it != ages.end(); ++it) {
|
||||
if (it->nodeNum == nodeNum) {
|
||||
ages.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Age a;
|
||||
a.nodeNum = nodeNum;
|
||||
a.seenAtMs = millis();
|
||||
|
||||
ages.push_front(a);
|
||||
}
|
||||
|
||||
// Remove Card and Age info for any nodes which are now inactive
|
||||
// Determined by when a node was last heard, in our internal record (not from nodeDB)
|
||||
void InkHUD::RecentsListApplet::prune()
|
||||
{
|
||||
// Iterate age records from newest to oldest
|
||||
for (uint16_t i = 0; i < ages.size(); i++) {
|
||||
// Found the first record which is too old
|
||||
if (!isActive(ages.at(i).seenAtMs)) {
|
||||
// Drop this item, and all others behind it
|
||||
ages.resize(i);
|
||||
cards.resize(i);
|
||||
|
||||
// Request an update, if pruning did modify our data
|
||||
// Required if pruning was scheduled. Redundent if pruning was prior to rendering.
|
||||
requestAutoshow();
|
||||
requestUpdate();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Push next scheduled pruning back
|
||||
// Pruning may be called from by handleParsed, immediately prior to rendering
|
||||
// In that case, we can slightly delay our scheduled pruning
|
||||
OSThread::setIntervalFromNow(60 * 1000UL);
|
||||
}
|
||||
|
||||
// Is a timestamp old enough that it would make a node inactive, and in need of purging?
|
||||
bool InkHUD::RecentsListApplet::isActive(uint32_t seenAtMs)
|
||||
{
|
||||
uint32_t now = millis();
|
||||
uint32_t secsAgo = (now - seenAtMs) / 1000UL; // millis() overflow safe
|
||||
|
||||
return (secsAgo < settings.recentlyActiveSeconds);
|
||||
}
|
||||
|
||||
// Text to be shown at top of applet
|
||||
// ChronoListApplet base class allows us to set this dynamically
|
||||
// Might want to adjust depending on node count, RTC status, etc
|
||||
std::string InkHUD::RecentsListApplet::getHeaderText()
|
||||
{
|
||||
std::string text;
|
||||
|
||||
// Print the length of our "Recents" time-window
|
||||
text += "Last ";
|
||||
text += to_string(settings.recentlyActiveSeconds / 60);
|
||||
text += " mins";
|
||||
|
||||
// Print the node count
|
||||
const uint16_t nodeCount = ages.size();
|
||||
text += ": ";
|
||||
text += to_string(nodeCount);
|
||||
text += " ";
|
||||
text += (nodeCount == 1) ? "node" : "nodes";
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,52 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Shows a list of nodes which have been recently active
|
||||
The length of this "recently active" window is configurable using the onscreen menu
|
||||
|
||||
Most of the work is done by the shared InkHUD::NodeListApplet base class
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class RecentsListApplet : public NodeListApplet, public concurrency::OSThread
|
||||
{
|
||||
protected:
|
||||
// Used internally to count the number of active nodes
|
||||
// We count for ourselves, instead of using the value provided by NodeDB,
|
||||
// as the values occasionally differ, due to the timing of our Applet's purge method
|
||||
struct Age {
|
||||
uint32_t nodeNum;
|
||||
uint32_t seenAtMs;
|
||||
};
|
||||
|
||||
public:
|
||||
RecentsListApplet();
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override;
|
||||
|
||||
void handleParsed(CardInfo c) override; // Store new info, update active count, update display if needed
|
||||
std::string getHeaderText() override; // Set title for this applet
|
||||
|
||||
void seenNow(NodeNum nodeNum); // Record that we have just seen this node, for active node count
|
||||
void prune(); // Remove cards for nodes which we haven't seen recently
|
||||
bool isActive(uint32_t seenAtMillis); // Is a node still active, based on when we last heard it?
|
||||
|
||||
std::deque<Age> ages; // Information about when we last heard nodes. Independent of NodeDB
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,270 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./ThreadedMessageApplet.h"
|
||||
|
||||
#include "RTC.h"
|
||||
#include "mesh/NodeDB.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
// Hard limits on how much message data to write to flash
|
||||
// Avoid filling the storage if something goes wrong
|
||||
// Normal usage should be well below this size
|
||||
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
|
||||
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
|
||||
|
||||
InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) : channelIndex(channelIndex)
|
||||
{
|
||||
// Create the message store
|
||||
// Will shortly attempt to load messages from RAM, if applet is active
|
||||
// Label (filename in flash) is set from channel index
|
||||
store = new MessageStore("ch" + to_string(channelIndex));
|
||||
}
|
||||
|
||||
void InkHUD::ThreadedMessageApplet::onRender()
|
||||
{
|
||||
setFont(fontSmall);
|
||||
|
||||
// =============
|
||||
// Draw a header
|
||||
// =============
|
||||
|
||||
// Header text
|
||||
std::string headerText;
|
||||
headerText += "Channel ";
|
||||
headerText += to_string(channelIndex);
|
||||
headerText += ": ";
|
||||
if (channels.isDefaultChannel(channelIndex))
|
||||
headerText += "Public";
|
||||
else
|
||||
headerText += channels.getByIndex(channelIndex).settings.name;
|
||||
|
||||
// Draw a "standard" applet header
|
||||
drawHeader(headerText);
|
||||
|
||||
// Y position for divider
|
||||
const int16_t dividerY = Applet::getHeaderHeight() - 1;
|
||||
|
||||
// ==================
|
||||
// Draw each message
|
||||
// ==================
|
||||
|
||||
// Restrict drawing area
|
||||
// - don't overdraw the header
|
||||
// - small gap below divider
|
||||
setCrop(0, dividerY + 2, width(), height() - (dividerY + 2));
|
||||
|
||||
// Set padding
|
||||
// - separates text from the vertical line which marks its edge
|
||||
constexpr uint16_t padW = 2;
|
||||
constexpr int16_t msgL = padW;
|
||||
const int16_t msgR = (width() - 1) - padW;
|
||||
const uint16_t msgW = (msgR - msgL) + 1;
|
||||
|
||||
int16_t msgB = height() - 1; // Vertical cursor for drawing. Messages are bottom-aligned to this value.
|
||||
uint8_t i = 0; // Index of stored message
|
||||
|
||||
// Loop over messages
|
||||
// - until no messages left, or
|
||||
// - until no part of message fits on screen
|
||||
while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) {
|
||||
|
||||
// Grab data for message
|
||||
MessageStore::Message &m = store->messages.at(i);
|
||||
bool outgoing = (m.sender == 0);
|
||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender);
|
||||
|
||||
// Cache bottom Y of message text
|
||||
// - Used when drawing vertical line alongside
|
||||
const int16_t dotsB = msgB;
|
||||
|
||||
// Get dimensions for message text
|
||||
uint16_t bodyH = getWrappedTextHeight(msgL, msgW, m.text);
|
||||
int16_t bodyT = msgB - bodyH;
|
||||
|
||||
// Print message
|
||||
// - if incoming
|
||||
if (!outgoing)
|
||||
printWrapped(msgL, bodyT, msgW, m.text);
|
||||
|
||||
// Print message
|
||||
// - if outgoing
|
||||
else {
|
||||
if (getTextWidth(m.text) < width()) // If short,
|
||||
printAt(msgR, bodyT, m.text, RIGHT); // print right align
|
||||
else // If long,
|
||||
printWrapped(msgL, bodyT, msgW, m.text); // need printWrapped(), which doesn't support right align
|
||||
}
|
||||
|
||||
// Move cursor up
|
||||
// - above message text
|
||||
msgB -= bodyH;
|
||||
msgB -= getFont().lineHeight() * 0.2; // Padding between message and header
|
||||
|
||||
// Compose info string
|
||||
// - shortname, if possible, or "me"
|
||||
// - time received, if possible
|
||||
std::string info;
|
||||
if (sender && sender->has_user)
|
||||
info += sender->user.short_name;
|
||||
else if (outgoing)
|
||||
info += "Me";
|
||||
else
|
||||
info += hexifyNodeNum(m.sender);
|
||||
|
||||
std::string timeString = getTimeString(m.timestamp);
|
||||
if (timeString.length() > 0) {
|
||||
info += " - ";
|
||||
info += timeString;
|
||||
}
|
||||
|
||||
// Print the info string
|
||||
// - Faux bold: printed twice, shifted horizontally by one px
|
||||
printAt(outgoing ? msgR : msgL, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM);
|
||||
printAt(outgoing ? msgR - 1 : msgL + 1, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM);
|
||||
|
||||
// Underline the info string
|
||||
const int16_t divY = msgB;
|
||||
int16_t divL;
|
||||
int16_t divR;
|
||||
if (!outgoing) {
|
||||
// Left side - incoming
|
||||
divL = msgL;
|
||||
divR = getTextWidth(info) + getFont().lineHeight() / 2;
|
||||
} else {
|
||||
// Right side - outgoing
|
||||
divR = msgR;
|
||||
divL = divR - getTextWidth(info) - getFont().lineHeight() / 2;
|
||||
}
|
||||
for (int16_t x = divL; x <= divR; x += 2)
|
||||
drawPixel(x, divY, BLACK);
|
||||
|
||||
// Move cursor up: above info string
|
||||
msgB -= fontSmall.lineHeight();
|
||||
|
||||
// Vertical line alongside message
|
||||
for (int16_t y = msgB; y < dotsB; y += 1)
|
||||
drawPixel(outgoing ? width() - 1 : 0, y, BLACK);
|
||||
|
||||
// Move cursor up: padding before next message
|
||||
msgB -= fontSmall.lineHeight() * 0.5;
|
||||
|
||||
i++;
|
||||
} // End of loop: drawing each message
|
||||
|
||||
// Fade effect:
|
||||
// Area immediately below the divider. Overdraw with sparse white lines.
|
||||
// Make text appear to pass behind the header
|
||||
hatchRegion(0, dividerY + 1, width(), fontSmall.lineHeight() / 3, 2, WHITE);
|
||||
|
||||
// If we've run out of screen to draw messages, we can drop any leftover data from the queue
|
||||
// Those messages have been pushed off the screen-top by newer ones
|
||||
while (i < store->messages.size())
|
||||
store->messages.pop_back();
|
||||
}
|
||||
|
||||
// Code which runs when the applet begins running
|
||||
// This might happen at boot, or if user enables the applet at run-time, via the menu
|
||||
void InkHUD::ThreadedMessageApplet::onActivate()
|
||||
{
|
||||
loadMessagesFromFlash();
|
||||
textMessageObserver.observe(textMessageModule); // Begin handling any new text messages with onReceiveTextMessage
|
||||
}
|
||||
|
||||
// Code which runs when the applet stop running
|
||||
// This might be happen at shutdown, or if user disables the applet at run-time
|
||||
void InkHUD::ThreadedMessageApplet::onDeactivate()
|
||||
{
|
||||
textMessageObserver.unobserve(textMessageModule); // Stop handling any new text messages with onReceiveTextMessage
|
||||
}
|
||||
|
||||
// Handle new text messages
|
||||
// These might be incoming, from the mesh, or outgoing from phone
|
||||
// Each instance of the ThreadMessageApplet will only listen on one specific channel
|
||||
// Method should return 0, to indicate general success to TextMessageModule
|
||||
int InkHUD::ThreadedMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
// Abort if applet fully deactivated
|
||||
// Already handled by onActivate and onDeactivate, but good practice for all applets
|
||||
if (!isActive())
|
||||
return 0;
|
||||
|
||||
// Abort if wrong channel
|
||||
if (p->channel != this->channelIndex)
|
||||
return 0;
|
||||
|
||||
// Abort if message was a DM
|
||||
if (p->to != NODENUM_BROADCAST)
|
||||
return 0;
|
||||
|
||||
// Abort if messages was an "emoji reaction"
|
||||
// Possibly some implemetation of this in future?
|
||||
if (p->decoded.emoji)
|
||||
return 0;
|
||||
|
||||
// Extract info into our slimmed-down "StoredMessage" type
|
||||
MessageStore::Message newMessage;
|
||||
newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
||||
newMessage.sender = p->from;
|
||||
newMessage.channelIndex = p->channel;
|
||||
newMessage.text = std::string(&p->decoded.payload.bytes[0], &p->decoded.payload.bytes[p->decoded.payload.size]);
|
||||
|
||||
// Store newest message at front
|
||||
// These records are used when rendering, and also stored in flash at shutdown
|
||||
store->messages.push_front(newMessage);
|
||||
|
||||
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
|
||||
if (getFrom(p) != nodeDB->getNodeNum())
|
||||
requestAutoshow();
|
||||
|
||||
// Redraw the applet, perhaps.
|
||||
requestUpdate(); // Want to update display, if applet is foreground
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Don't show notifications for text messages broadcast to our channel, when the applet is displayed
|
||||
bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n)
|
||||
{
|
||||
if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST && n.getChannel() == channelIndex)
|
||||
return false;
|
||||
|
||||
// None of our business. Allow the notification.
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
// Save several recent messages to flash
|
||||
// Stores the contents of ThreadedMessageApplet::messages
|
||||
// Just enough messages to fill the display
|
||||
// Messages are packed "back-to-back", to minimize blocks of flash used
|
||||
void InkHUD::ThreadedMessageApplet::saveMessagesToFlash()
|
||||
{
|
||||
// Create a label (will become the filename in flash)
|
||||
std::string label = "ch" + to_string(channelIndex);
|
||||
|
||||
store->saveToFlash();
|
||||
}
|
||||
|
||||
// Load recent messages to flash
|
||||
// Fills ThreadedMessageApplet::messages with previous messages
|
||||
// Just enough messages have been stored to cover the display
|
||||
void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash()
|
||||
{
|
||||
// Create a label (will become the filename in flash)
|
||||
std::string label = "ch" + to_string(channelIndex);
|
||||
|
||||
store->loadFromFlash();
|
||||
}
|
||||
|
||||
// Code to run when device is shutting down
|
||||
// This is in addition to any onDeactivate() code, which will also run
|
||||
// Todo: implement before a reboot also
|
||||
void InkHUD::ThreadedMessageApplet::onShutdown()
|
||||
{
|
||||
// Save our current set of messages to flash, provided the applet isn't disabled
|
||||
if (isActive())
|
||||
saveMessagesToFlash();
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,63 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Displays a thread-view of incoming and outgoing message for a specific channel
|
||||
|
||||
The channel for this applet is set in the constructor,
|
||||
when the applet is added to WindowManager in the setupNicheGraphics method.
|
||||
|
||||
Several messages are saved to flash at shutdown, to preseve applet between reboots.
|
||||
This class has its own internal method for saving and loading to fs, which interacts directly with the FSCommon layer.
|
||||
If the amount of flash usage is unacceptable, we could keep these in RAM only.
|
||||
|
||||
Multiple instances of this channel may be used. This must be done at buildtime.
|
||||
Suggest a max of two channel, to minimize fs usage?
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
#include "graphics/niche/InkHUD/MessageStore.h"
|
||||
|
||||
#include "modules/TextMessageModule.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class Applet;
|
||||
|
||||
class ThreadedMessageApplet : public Applet
|
||||
{
|
||||
public:
|
||||
ThreadedMessageApplet(uint8_t channelIndex);
|
||||
ThreadedMessageApplet() = delete;
|
||||
|
||||
void onRender() override;
|
||||
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
void onShutdown() override;
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
|
||||
|
||||
bool approveNotification(Notification &n) override; // Which notifications to suppress
|
||||
|
||||
protected:
|
||||
// Used to register our text message callback
|
||||
CallbackObserver<ThreadedMessageApplet, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<ThreadedMessageApplet, const meshtastic_MeshPacket *>(this,
|
||||
&ThreadedMessageApplet::onReceiveTextMessage);
|
||||
|
||||
void saveMessagesToFlash();
|
||||
void loadMessagesFromFlash();
|
||||
|
||||
MessageStore *store; // Messages, held in RAM for use, ready to save to flash on shutdown
|
||||
uint8_t channelIndex = 0;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,139 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./MessageStore.h"
|
||||
|
||||
#include "SafeFile.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
// Hard limits on how much message data to write to flash
|
||||
// Avoid filling the storage if something goes wrong
|
||||
// Normal usage should be well below this size
|
||||
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
|
||||
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
|
||||
|
||||
InkHUD::MessageStore::MessageStore(std::string label)
|
||||
{
|
||||
filename = "";
|
||||
filename += "/NicheGraphics";
|
||||
filename += "/";
|
||||
filename += label;
|
||||
filename += ".msgs";
|
||||
}
|
||||
|
||||
// Write the contents of the MessageStore::messages object to flash
|
||||
void InkHUD::MessageStore::saveToFlash()
|
||||
{
|
||||
assert(!filename.empty());
|
||||
|
||||
#ifdef FSCom
|
||||
// Make the directory, if doesn't already exist
|
||||
// This is the same directory accessed by NicheGraphics::FlashData
|
||||
FSCom.mkdir("/NicheGraphics");
|
||||
|
||||
// Open or create the file
|
||||
// No "full atomic": don't save then rename
|
||||
auto f = SafeFile(filename.c_str(), false);
|
||||
|
||||
LOG_INFO("Saving messages in %s", filename.c_str());
|
||||
|
||||
// 1st byte: how many messages will be written to store
|
||||
f.write(messages.size());
|
||||
|
||||
// For each message
|
||||
for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) {
|
||||
Message &m = messages.at(i);
|
||||
f.write((uint8_t *)&m.timestamp, sizeof(m.timestamp)); // Write timestamp. 4 bytes
|
||||
f.write((uint8_t *)&m.sender, sizeof(m.sender)); // Write sender NodeId. 4 Bytes
|
||||
f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); // Write channel index. 1 Byte
|
||||
f.write((uint8_t *)m.text.c_str(), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text. Variable length
|
||||
f.write('\0'); // Append null term
|
||||
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", (uint32_t)i, min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str());
|
||||
}
|
||||
|
||||
bool writeSucceeded = f.close();
|
||||
|
||||
if (!writeSucceeded) {
|
||||
LOG_ERROR("Can't write data!");
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("ERROR: Filesystem not implemented\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
// Attempt to load the previous contents of the MessageStore:message deque from flash.
|
||||
// Filename is controlled by the "label" parameter
|
||||
void InkHUD::MessageStore::loadFromFlash()
|
||||
{
|
||||
// Hopefully redundant. Initial intention is to only load / save once per boot.
|
||||
messages.clear();
|
||||
|
||||
#ifdef FSCom
|
||||
|
||||
// Check that the file *does* actually exist
|
||||
if (!FSCom.exists(filename.c_str())) {
|
||||
LOG_WARN("'%s' not found. Using default values", filename.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that the file *does* actually exist
|
||||
if (!FSCom.exists(filename.c_str())) {
|
||||
LOG_INFO("'%s' not found.", filename.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the file
|
||||
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
|
||||
|
||||
if (f.size() == 0) {
|
||||
LOG_INFO("%s is empty", filename.c_str());
|
||||
f.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// If opened, start reading
|
||||
if (f) {
|
||||
LOG_INFO("Loading threaded messages '%s'", filename.c_str());
|
||||
|
||||
// First byte: how many messages are in the flash store
|
||||
uint8_t flashMessageCount = 0;
|
||||
f.readBytes((char *)&flashMessageCount, 1);
|
||||
LOG_DEBUG("Messages available: %u", (uint32_t)flashMessageCount);
|
||||
|
||||
// For each message
|
||||
for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) {
|
||||
Message m;
|
||||
|
||||
// Read meta data (fixed width)
|
||||
f.readBytes((char *)&m.timestamp, sizeof(m.timestamp));
|
||||
f.readBytes((char *)&m.sender, sizeof(m.sender));
|
||||
f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex));
|
||||
|
||||
// Read characters until we find a null term
|
||||
char c;
|
||||
while (m.text.size() < MAX_MESSAGE_SIZE) {
|
||||
f.readBytes(&c, 1);
|
||||
if (c != '\0')
|
||||
m.text += c;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
// Store in RAM
|
||||
messages.push_back(m);
|
||||
|
||||
LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", (uint32_t)i, m.timestamp, m.sender, m.text.c_str());
|
||||
}
|
||||
|
||||
f.close();
|
||||
} else {
|
||||
LOG_ERROR("Could not open / read %s", filename.c_str());
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("Filesystem not implemented");
|
||||
state = LoadFileState::NO_FILESYSTEM;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,47 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
We hold a few recent messages, for features like the threaded message applet.
|
||||
This class contains a struct for storing those messages,
|
||||
and methods for serializing them to flash.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include <deque>
|
||||
|
||||
#include "mesh/MeshTypes.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class MessageStore
|
||||
{
|
||||
public:
|
||||
// A stored message
|
||||
struct Message {
|
||||
uint32_t timestamp; // Epoch seconds
|
||||
NodeNum sender = 0;
|
||||
uint8_t channelIndex;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
MessageStore() = delete;
|
||||
MessageStore(std::string label); // Label determines filename in flash
|
||||
|
||||
void saveToFlash();
|
||||
void loadFromFlash();
|
||||
|
||||
std::deque<Message> messages; // Interact with this object!
|
||||
|
||||
private:
|
||||
std::string filename;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,59 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./Persistence.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
// Load settings and latestMessage data
|
||||
void InkHUD::loadDataFromFlash()
|
||||
{
|
||||
// Load the InkHUD settings from flash, and check version number
|
||||
// We should only consider the version number if the InkHUD flashdata component reports that we *did* actually load flash data
|
||||
InkHUD::Settings loadedSettings;
|
||||
bool loadSucceeded = FlashData<Settings>::load(&loadedSettings, "settings");
|
||||
if (loadSucceeded && loadedSettings.meta.version == SETTINGS_VERSION && loadedSettings.meta.version != 0)
|
||||
settings = loadedSettings; // Version matched, replace the defaults with the loaded values
|
||||
else
|
||||
LOG_WARN("Settings version changed. Using defaults");
|
||||
|
||||
// Load previous "latestMessages" data from flash
|
||||
MessageStore store("latest");
|
||||
store.loadFromFlash();
|
||||
|
||||
// Place into latestMessage struct, for convenient access
|
||||
// Number of strings loaded determines whether last message was broadcast or dm
|
||||
if (store.messages.size() == 1) {
|
||||
latestMessage.dm = store.messages.at(0);
|
||||
latestMessage.wasBroadcast = false;
|
||||
} else if (store.messages.size() == 2) {
|
||||
latestMessage.dm = store.messages.at(0);
|
||||
latestMessage.broadcast = store.messages.at(1);
|
||||
latestMessage.wasBroadcast = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Save settings and latestMessage data
|
||||
void InkHUD::saveDataToFlash()
|
||||
{
|
||||
// Save the InkHUD settings to flash
|
||||
FlashData<Settings>::save(&settings, "settings");
|
||||
|
||||
// Number of strings saved determines whether last message was broadcast or dm
|
||||
MessageStore store("latest");
|
||||
store.messages.push_back(latestMessage.dm);
|
||||
if (latestMessage.wasBroadcast)
|
||||
store.messages.push_back(latestMessage.broadcast);
|
||||
store.saveToFlash();
|
||||
}
|
||||
|
||||
// Holds InkHUD settings while running
|
||||
// Saved back to Flash at shutdown
|
||||
// Accessed by including persistence.h
|
||||
InkHUD::Settings InkHUD::settings;
|
||||
|
||||
// Holds copies of the most recent broadcast and DM messages while running
|
||||
// Saved to Flash at shutdown
|
||||
// Accessed by including persistence.h
|
||||
InkHUD::LatestMessage InkHUD::latestMessage;
|
||||
|
||||
#endif
|
|
@ -0,0 +1,123 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
A quick and dirty alternative to storing "device only" settings using the protobufs
|
||||
Convenient during development.
|
||||
Potentially a polite option, to avoid polluting the generated code with values for obscure use cases like this.
|
||||
|
||||
The save / load mechanism is a shared NicheGraphics feature.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/FlashData.h"
|
||||
#include "graphics/niche/InkHUD/MessageStore.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
constexpr uint8_t MAX_TILES_GLOBAL = 4;
|
||||
constexpr uint8_t MAX_USERAPPLETS_GLOBAL = 16;
|
||||
|
||||
// Used to invalidate old settings, if needed
|
||||
// Version 0 is reserved for testing, and will always load defaults
|
||||
constexpr uint32_t SETTINGS_VERSION = 2;
|
||||
|
||||
struct Settings {
|
||||
struct Meta {
|
||||
// Used to invalidate old savefiles, if we make breaking changes
|
||||
uint32_t version = SETTINGS_VERSION;
|
||||
} meta;
|
||||
|
||||
struct UserTiles {
|
||||
// How many tiles are shown
|
||||
uint8_t count = 1;
|
||||
|
||||
// Maximum amount of tiles for this display
|
||||
uint8_t maxCount = 4;
|
||||
|
||||
// Which tile is focused (responding to user button input)
|
||||
uint8_t focused = 0;
|
||||
|
||||
// Which applet is displayed on which tile
|
||||
// Index of array: which tile, as indexed in WindowManager::tiles
|
||||
// Value of array: which applet, as indexed in WindowManager::activeApplets
|
||||
uint8_t displayedUserApplet[MAX_TILES_GLOBAL] = {0, 1, 2, 3};
|
||||
} userTiles;
|
||||
|
||||
struct UserApplets {
|
||||
// Which applets are running (either displayed, or in the background)
|
||||
// Index of array: which applet, as indexed in WindowManager::applets
|
||||
// Initial value is set by the "activeByDefault" parameter of WindowManager::addApplet, in setupNicheGraphics()
|
||||
bool active[MAX_USERAPPLETS_GLOBAL];
|
||||
|
||||
// Which user applets should be automatically shown when they have important data to show
|
||||
// If none set, foreground applets should remain foreground without manual user input
|
||||
// If multiple applets request this at once,
|
||||
// priority is the order which they were passed to WindowManager::addApplets, in setupNicheGraphics()
|
||||
bool autoshow[MAX_USERAPPLETS_GLOBAL]{false};
|
||||
} userApplets;
|
||||
|
||||
// Features which the use can enable / disable via the on-screen menu
|
||||
struct OptionalFeatures {
|
||||
bool notifications = true;
|
||||
bool batteryIcon = false;
|
||||
} optionalFeatures;
|
||||
|
||||
// Some menu items may not be required, based on device / configuration
|
||||
// We can enable them only when needed, to de-clutter the menu
|
||||
struct OptionalMenuItems {
|
||||
// If aux button is used to swap between tiles, we have to need for this menu item
|
||||
bool nextTile = true;
|
||||
|
||||
// Used if backlight present, and not controlled by AUX button
|
||||
// If this item is added to menu: backlight is always active when menu is open
|
||||
// The added menu items then allows the user to "Keep Backlight On", globally.
|
||||
bool backlight = false;
|
||||
} optionalMenuItems;
|
||||
|
||||
// Allows tips to be run once only
|
||||
struct Tips {
|
||||
// Enables the longer "tutorial" shown only on first boot
|
||||
// Once tutorial has been completed, it is no longer shown
|
||||
bool firstBoot = true;
|
||||
|
||||
// User is advised to shutdown before removing device power
|
||||
// Once user executes a shutdown (either via menu or client app),
|
||||
// this tip is no longer shown
|
||||
bool safeShutdownSeen = false;
|
||||
} tips;
|
||||
|
||||
// Rotation of the display
|
||||
// Multiples of 90 degrees clockwise
|
||||
// Most commonly: rotation is 0 when flex connector is oriented below display
|
||||
uint8_t rotation = 1;
|
||||
|
||||
// How long do we consider another node to be "active"?
|
||||
// Used when applets want to filter for "active nodes" only
|
||||
uint32_t recentlyActiveSeconds = 2 * 60;
|
||||
};
|
||||
|
||||
// Most recently received text message
|
||||
// Value is updated by InkHUD::WindowManager, as a courtesty to applets
|
||||
// Note: different from devicestate.rx_text_message,
|
||||
// which may contain an *outgoing message* to broadcast
|
||||
struct LatestMessage {
|
||||
MessageStore::Message broadcast; // Most recent message received broadcast
|
||||
MessageStore::Message dm; // Most recent received DM
|
||||
bool wasBroadcast; // True if most recent broadcast is newer than most recent dm
|
||||
};
|
||||
|
||||
extern Settings settings;
|
||||
extern LatestMessage latestMessage;
|
||||
|
||||
void loadDataFromFlash();
|
||||
void saveDataToFlash();
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,10 @@
|
|||
[inkhud]
|
||||
board_level = extra
|
||||
build_src_filter = +<../variants/$PIOENV> ; Include nicheGraphics.h
|
||||
build_flags =
|
||||
-D MESHTASTIC_INCLUDE_NICHE_GRAPHICS ; Use NicheGraphics
|
||||
-D MESHTASTIC_INCLUDE_INKHUD ; Use InkHUD (a NicheGraphics UI)
|
||||
-D MESHTASTIC_EXCLUDE_SCREEN ; Suppress default Screen class
|
||||
-D HAS_BUTTON=0 ; Suppress default ButtonThread
|
||||
lib_deps =
|
||||
https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX
|
|
@ -0,0 +1,12 @@
|
|||
# InkHUD
|
||||
|
||||
A heads-up-display for E-Ink devices, intended to supplement a connected phone / client. Implemented as a "NicheGraphics" UI.
|
||||
|
||||
Supported devices (as of 1st Feb. 2025):
|
||||
|
||||
- Heltec Vision Master E213
|
||||
- Heltec Vision Master E290
|
||||
- Heltec Wireless Paper V1.1
|
||||
- LILYGO T-Echo
|
||||
|
||||
More to follow
|
|
@ -0,0 +1,237 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./Tile.h"
|
||||
|
||||
#include "concurrency/Periodic.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
// Static members of Tile class (for linking)
|
||||
InkHUD::Tile *InkHUD::Tile::highlightTarget;
|
||||
bool InkHUD::Tile::highlightShown;
|
||||
|
||||
// For dismissing the highlight indicator, after a few seconds
|
||||
// Highlighting is used to inform user of which tile is now focused
|
||||
static concurrency::Periodic *taskHighlight;
|
||||
static int32_t runtaskHighlight()
|
||||
{
|
||||
LOG_DEBUG("Dismissing Highlight");
|
||||
InkHUD::Tile::highlightShown = false;
|
||||
InkHUD::Tile::highlightTarget = nullptr;
|
||||
InkHUD::WindowManager::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting
|
||||
return taskHighlight->disable();
|
||||
}
|
||||
static void inittaskHighlight()
|
||||
{
|
||||
static bool doneOnce = false;
|
||||
if (!doneOnce) {
|
||||
taskHighlight = new concurrency::Periodic("Highlight", runtaskHighlight);
|
||||
taskHighlight->disable();
|
||||
doneOnce = true;
|
||||
}
|
||||
}
|
||||
|
||||
InkHUD::Tile::Tile()
|
||||
{
|
||||
// For convenince
|
||||
windowManager = InkHUD::WindowManager::getInstance();
|
||||
|
||||
inittaskHighlight();
|
||||
Tile::highlightTarget = nullptr;
|
||||
Tile::highlightShown = false;
|
||||
}
|
||||
|
||||
// Set the region of the tile automatically, based on the user's chosen layout
|
||||
// This method places tiles which will host user applets
|
||||
// The WindowManager multiplexes the applets to these tiles automatically
|
||||
void InkHUD::Tile::placeUserTile(uint8_t userTileCount, uint8_t tileIndex)
|
||||
{
|
||||
uint16_t displayWidth = windowManager->getWidth();
|
||||
uint16_t displayHeight = windowManager->getHeight();
|
||||
|
||||
bool landscape = displayWidth > displayHeight;
|
||||
|
||||
// Check for any stray tiles
|
||||
if (tileIndex > (userTileCount - 1)) {
|
||||
// Dummy values to prevent rendering
|
||||
LOG_WARN("Tile index out of bounds");
|
||||
left = -2;
|
||||
top = -2;
|
||||
width = 1;
|
||||
height = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Todo: special handling for the notification area
|
||||
// Todo: special handling for 3 tile layout
|
||||
|
||||
// Gap between tiles
|
||||
const uint16_t spacing = 4;
|
||||
|
||||
switch (userTileCount) {
|
||||
// One tile only
|
||||
case 1:
|
||||
left = 0;
|
||||
top = 0;
|
||||
width = displayWidth;
|
||||
height = displayHeight;
|
||||
break;
|
||||
|
||||
// Two tiles
|
||||
case 2:
|
||||
if (landscape) {
|
||||
// Side by side
|
||||
left = ((displayWidth / 2) + (spacing / 2)) * tileIndex;
|
||||
top = 0;
|
||||
width = (displayWidth / 2) - (spacing / 2);
|
||||
height = displayHeight;
|
||||
} else {
|
||||
// Above and below
|
||||
left = 0;
|
||||
top = 0 + (((displayHeight / 2) + (spacing / 2)) * tileIndex);
|
||||
width = displayWidth;
|
||||
height = (displayHeight / 2) - (spacing / 2);
|
||||
}
|
||||
break;
|
||||
|
||||
// Four tiles
|
||||
case 4:
|
||||
width = (displayWidth / 2) - (spacing / 2);
|
||||
height = (displayHeight / 2) - (spacing / 2);
|
||||
switch (tileIndex) {
|
||||
case 0:
|
||||
left = 0;
|
||||
top = 0;
|
||||
break;
|
||||
case 1:
|
||||
left = 0 + (width - 1) + spacing;
|
||||
top = 0;
|
||||
break;
|
||||
case 2:
|
||||
left = 0;
|
||||
top = 0 + (height - 1) + spacing;
|
||||
break;
|
||||
case 3:
|
||||
left = 0 + (width - 1) + spacing;
|
||||
top = 0 + (height - 1) + spacing;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG_ERROR("Unsupported tile layout");
|
||||
assert(0);
|
||||
}
|
||||
|
||||
assert(width > 0 && height > 0);
|
||||
|
||||
this->left = left;
|
||||
this->top = top;
|
||||
this->width = width;
|
||||
this->height = height;
|
||||
}
|
||||
|
||||
// Manually set the region for a tile
|
||||
// This is only done for tiles which will host certain "System Applets", which have unique position / sizes:
|
||||
// Things like the NotificationApplet, BatteryIconApplet, etc
|
||||
void InkHUD::Tile::placeSystemTile(int16_t left, int16_t top, uint16_t width, uint16_t height)
|
||||
{
|
||||
assert(width > 0 && height > 0);
|
||||
|
||||
this->left = left;
|
||||
this->top = top;
|
||||
this->width = width;
|
||||
this->height = height;
|
||||
}
|
||||
|
||||
// Place an applet onto a tile
|
||||
// Creates a reciprocal link between applet and tile
|
||||
// The tile should always know which applet is displayed
|
||||
// The applet should always know which tile it is display on
|
||||
// This is enforced with asserts
|
||||
// Assigning a new applet will break a previous link
|
||||
// Link may also be broken by assigning a nullptr
|
||||
void InkHUD::Tile::assignApplet(Applet *a)
|
||||
{
|
||||
// Break the link between old applet and this tile
|
||||
if (assignedApplet)
|
||||
assignedApplet->setTile(nullptr);
|
||||
|
||||
// Store the new applet
|
||||
assignedApplet = a;
|
||||
|
||||
// Create the reciprocal link between the new applet and this tile
|
||||
if (a)
|
||||
a->setTile(this);
|
||||
}
|
||||
|
||||
// Get pointer to whichever applet is displayed on this tile
|
||||
InkHUD::Applet *InkHUD::Tile::getAssignedApplet()
|
||||
{
|
||||
return assignedApplet;
|
||||
}
|
||||
|
||||
// Receive drawing output from the assigned applet,
|
||||
// and translate it from "applet-space" coordinates, to it's true location.
|
||||
// The final "rotation" step is performed by the windowManager
|
||||
void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c)
|
||||
{
|
||||
// Move pixels from applet-space to tile-space
|
||||
x += left;
|
||||
y += top;
|
||||
|
||||
// Crop to tile borders
|
||||
if (x >= left && x < (left + width) && y >= top && y < (top + height)) {
|
||||
// Pass to the window manager
|
||||
windowManager->handleTilePixel(x, y, c);
|
||||
}
|
||||
}
|
||||
|
||||
// Called by Applet base class, when learning of its dimensions
|
||||
uint16_t InkHUD::Tile::getWidth()
|
||||
{
|
||||
return width;
|
||||
}
|
||||
|
||||
// Called by Applet base class, when learning of its dimensions
|
||||
uint16_t InkHUD::Tile::getHeight()
|
||||
{
|
||||
return height;
|
||||
}
|
||||
|
||||
// Longest edge of the display, in pixels
|
||||
// Maximum possible size of any tile's width / height
|
||||
// Used by some components to allocate resources for the "worst possible situtation"
|
||||
// "Sizing the cathedral for christmas eve"
|
||||
uint16_t InkHUD::Tile::maxDisplayDimension()
|
||||
{
|
||||
WindowManager *wm = WindowManager::getInstance();
|
||||
return max(wm->getHeight(), wm->getWidth());
|
||||
}
|
||||
|
||||
// Ask for this tile to be highlighted
|
||||
// Used to indicate which tile is now indicated after focus changes
|
||||
// Only used for aux button focus changes, not changes via menu
|
||||
void InkHUD::Tile::requestHighlight()
|
||||
{
|
||||
Tile::highlightTarget = this;
|
||||
Tile::highlightShown = false;
|
||||
windowManager->forceUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
// Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first
|
||||
void InkHUD::Tile::startHighlightTimeout()
|
||||
{
|
||||
taskHighlight->setIntervalFromNow(5 * 1000UL);
|
||||
taskHighlight->enabled = true;
|
||||
}
|
||||
|
||||
// Stop the timer which would automatically dismiss the highlighting
|
||||
// Called if the tile organically renders before the timer is up
|
||||
void InkHUD::Tile::cancelHighlightTimeout()
|
||||
{
|
||||
if (taskHighlight->enabled)
|
||||
taskHighlight->disable();
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,62 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Class which represents a region of the display area
|
||||
Applets are assigned to a tile
|
||||
Tile controls the Applet's dimensions
|
||||
Tile receives pixel output from the applet, and translates it to the correct display region
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./Applet.h"
|
||||
#include "./Types.h"
|
||||
#include "./WindowManager.h"
|
||||
|
||||
#include <GFX.h>
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class Applet;
|
||||
class WindowManager;
|
||||
|
||||
class Tile
|
||||
{
|
||||
public:
|
||||
Tile();
|
||||
void placeUserTile(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout
|
||||
void placeSystemTile(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
|
||||
uint16_t getWidth(); // Used to set the assigned applet's width before render
|
||||
uint16_t getHeight(); // Used to set the assigned applet's height before render
|
||||
static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter
|
||||
|
||||
void assignApplet(Applet *a); // Place an applet onto a tile
|
||||
Applet *getAssignedApplet(); // Applet which is on a tile
|
||||
|
||||
void requestHighlight(); // Ask for this tile to be highlighted
|
||||
static void startHighlightTimeout(); // Start the auto-dismissal timer
|
||||
static void cancelHighlightTimeout(); // Cancel the auto-dismissal timer early; already dismissed
|
||||
|
||||
static Tile *highlightTarget; // Which tile are we highlighting? (Intending to highlight?)
|
||||
static bool highlightShown; // Is the tile highlighted yet? Controlls highlight vs dismiss
|
||||
|
||||
protected:
|
||||
int16_t left;
|
||||
int16_t top;
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
|
||||
Applet *assignedApplet = nullptr; // Pointer to the applet which is currently linked with the tile
|
||||
|
||||
WindowManager *windowManager; // Convenient access to the WindowManager singleton
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,62 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Custom data types for InkHUD
|
||||
|
||||
Only "general purpose" data-types should be defined here.
|
||||
If your applet has its own structs or enums, which won't be useful to other applets,
|
||||
please define them inside (or in the same folder as) your applet.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/Drivers/EInk/EInk.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
// Color, understood by display controller IC (as bit values)
|
||||
// Also suitable for use as AdafruitGFX colors
|
||||
enum Color : uint8_t {
|
||||
BLACK = 0,
|
||||
WHITE = 1,
|
||||
};
|
||||
|
||||
// Info contained within AppletFont
|
||||
struct FontDimensions {
|
||||
uint8_t height;
|
||||
uint8_t ascenderHeight;
|
||||
uint8_t descenderHeight;
|
||||
};
|
||||
|
||||
// Which edge Applet::printAt will place on the X parameter
|
||||
enum HorizontalAlignment : uint8_t {
|
||||
LEFT,
|
||||
RIGHT,
|
||||
CENTER,
|
||||
};
|
||||
|
||||
// Which edge Applet::printAt will place on the Y parameter
|
||||
enum VerticalAlignment : uint8_t {
|
||||
TOP,
|
||||
MIDDLE,
|
||||
BOTTOM,
|
||||
};
|
||||
|
||||
// An easy-to-understand intepretation of SNR and RSSI
|
||||
// Calculate with Applet::getSignalStringth
|
||||
enum SignalStrength : int8_t {
|
||||
SIGNAL_UNKNOWN = -1,
|
||||
SIGNAL_NONE,
|
||||
SIGNAL_BAD,
|
||||
SIGNAL_FAIR,
|
||||
SIGNAL_GOOD,
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,151 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./UpdateMediator.h"
|
||||
|
||||
#include "./WindowManager.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL;
|
||||
static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL;
|
||||
|
||||
InkHUD::UpdateMediator::UpdateMediator() : concurrency::OSThread("Mediator")
|
||||
{
|
||||
// Timer disabled by default
|
||||
OSThread::disable();
|
||||
}
|
||||
|
||||
// Ask which type of update operation we should perform
|
||||
// Even if we explicitly want a FAST or FULL update, we should pass it through this method,
|
||||
// as it allows UpdateMediator to count the refreshes.
|
||||
// Internal "maintenance" refreshes are not passed through evaluate, however.
|
||||
Drivers::EInk::UpdateTypes InkHUD::UpdateMediator::evaluate(Drivers::EInk::UpdateTypes requested)
|
||||
{
|
||||
LOG_DEBUG("FULL-update debt:%f", debt);
|
||||
|
||||
// For conveninece
|
||||
typedef Drivers::EInk::UpdateTypes UpdateTypes;
|
||||
|
||||
// Check whether we've paid off enough debt to stop unprovoked refreshing (if in progress)
|
||||
// This maintenance behavior will also halt itself when the timer next fires,
|
||||
// but that could be an hour away, so we can stop it early here and free up resources
|
||||
if (OSThread::enabled && debt == 0.0)
|
||||
endMaintenance();
|
||||
|
||||
// Explicitly requested FULL
|
||||
if (requested == UpdateTypes::FULL) {
|
||||
LOG_DEBUG("Explicit FULL");
|
||||
debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt
|
||||
return UpdateTypes::FULL;
|
||||
}
|
||||
|
||||
// Explicitly requested FAST
|
||||
if (requested == UpdateTypes::FAST) {
|
||||
LOG_DEBUG("Explicit FAST");
|
||||
// Add to the FULL refresh debt
|
||||
if (debt < 1.0)
|
||||
debt += 1.0 / fastPerFull;
|
||||
else
|
||||
debt += stressMultiplier * (1.0 / fastPerFull); // More debt if too many consecutive FAST refreshes
|
||||
|
||||
// If *significant debt*, begin occasionally refreshing *unprovoked*
|
||||
// This maintenance behavior is only triggered here, during periods of user interaction
|
||||
if (debt >= 2.0)
|
||||
beginMaintenance();
|
||||
|
||||
return UpdateTypes::FAST; // Give them what the asked for
|
||||
}
|
||||
|
||||
// Handling UpdateTypes::UNSPECIFIED
|
||||
// -----------------------------------
|
||||
// In this case, the UI doesn't care which refresh we use
|
||||
|
||||
// Not much debt: suggest FAST
|
||||
if (debt < 1.0) {
|
||||
LOG_DEBUG("UNSPECIFIED: using FAST");
|
||||
debt += 1.0 / fastPerFull;
|
||||
return UpdateTypes::FAST;
|
||||
}
|
||||
|
||||
// In debt: suggest FULL
|
||||
else {
|
||||
LOG_DEBUG("UNSPECIFIED: using FULL");
|
||||
debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt
|
||||
|
||||
// When maintenance begins, the first refresh happens shortly after user interaction ceases (a minute or so)
|
||||
// If we *are* given an opportunity to refresh before that, we'll skip that initial maintenance refresh
|
||||
// We were intending to use that initial refresh to redraw the screen as FULL, but we're doing that now, organically
|
||||
if (OSThread::enabled && OSThread::interval == MAINTENANCE_MS_INITIAL) {
|
||||
LOG_DEBUG("Initial maintenance skipped");
|
||||
OSThread::setInterval(MAINTENANCE_MS); // Note: not intervalFromNow
|
||||
}
|
||||
|
||||
return UpdateTypes::FULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which of two update types is more important to honor
|
||||
// Explicit FAST is more important than UNSPECIFIED - prioritize responsiveness
|
||||
// Explicit FULL is more important than explicint FAST - prioritize image quality: explicit FULL is rare
|
||||
Drivers::EInk::UpdateTypes InkHUD::UpdateMediator::prioritize(Drivers::EInk::UpdateTypes type1, Drivers::EInk::UpdateTypes type2)
|
||||
{
|
||||
switch (type1) {
|
||||
case Drivers::EInk::UpdateTypes::UNSPECIFIED:
|
||||
return type2;
|
||||
|
||||
case Drivers::EInk::UpdateTypes::FAST:
|
||||
return (type2 == Drivers::EInk::UpdateTypes::FULL) ? Drivers::EInk::UpdateTypes::FULL : Drivers::EInk::UpdateTypes::FAST;
|
||||
|
||||
case Drivers::EInk::UpdateTypes::FULL:
|
||||
return type1;
|
||||
}
|
||||
|
||||
return Drivers::EInk::UpdateTypes::UNSPECIFIED; // Suppress compiler warning only
|
||||
}
|
||||
|
||||
// We're using the timer to perform "maintenance"
|
||||
// If signifcant FULL-refresh debt has accumulated, we will occasionally run FULL refreshes unprovoked.
|
||||
// This prevents gradual build-up of debt,
|
||||
// in case we don't have enough UNSPECIFIED refreshes to pay the debt back organically.
|
||||
// The first refresh takes place shortly after user finishes interacting with the device; this does the bulk of the restoration
|
||||
// Subsequent refreshes take place *much* less frequently.
|
||||
// Hopefully an applet will want to render before this, meaning we can cancel the maintenance.
|
||||
int32_t InkHUD::UpdateMediator::runOnce()
|
||||
{
|
||||
if (debt > 0.0) {
|
||||
LOG_DEBUG("debt=%f: performing maintenance", debt);
|
||||
|
||||
// Ask WindowManager to redraw everything, purely for the refresh
|
||||
// Todo: optimize? Could update without re-rendering
|
||||
WindowManager::getInstance()->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
|
||||
// Record that we have paid back (some of) the FULL refresh debt
|
||||
debt = max(debt - 1.0, 0.0);
|
||||
|
||||
// Next maintenance refresh scheduled - long wait (an hour?)
|
||||
return MAINTENANCE_MS;
|
||||
}
|
||||
|
||||
else
|
||||
return endMaintenance();
|
||||
}
|
||||
|
||||
// Begin periodically refreshing the display, to repay FULL-refresh debt
|
||||
// We do this in case user doesn't have enough activity to repay it organically, with UpdateTypes::UNSPECIFIED
|
||||
// After an initial refresh, to redraw as FULL, we only perform these maintenance refreshes very infrequently
|
||||
// This gives the display a chance to heal by evaluating UNSPECIFIED as FULL, which is preferable
|
||||
void InkHUD::UpdateMediator::beginMaintenance()
|
||||
{
|
||||
LOG_DEBUG("Maintenance enabled");
|
||||
OSThread::setIntervalFromNow(MAINTENANCE_MS_INITIAL);
|
||||
OSThread::enabled = true;
|
||||
}
|
||||
|
||||
// FULL-refresh debt is low enough that we no longer need to pay it back with periodic updates
|
||||
int32_t InkHUD::UpdateMediator::endMaintenance()
|
||||
{
|
||||
LOG_DEBUG("Maintenance disabled");
|
||||
return OSThread::disable();
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,45 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Responsible for display health
|
||||
- counts number of FULL vs FAST refresh
|
||||
- suggests whether to use FAST or FULL, when not explicitly specified
|
||||
- periodically requests update unprovoked, if required for display health
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/Drivers/EInk/EInk.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class UpdateMediator : protected concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
UpdateMediator();
|
||||
|
||||
// Tell the mediator what we want, get told what we can have
|
||||
Drivers::EInk::UpdateTypes evaluate(Drivers::EInk::UpdateTypes requested);
|
||||
|
||||
// Determine which of two update types is more important to honor
|
||||
Drivers::EInk::UpdateTypes prioritize(Drivers::EInk::UpdateTypes type1, Drivers::EInk::UpdateTypes type2);
|
||||
|
||||
uint8_t fastPerFull = 5; // Ideal number of fast refreshes between full refreshes
|
||||
float stressMultiplier = 2.0; // How bad for the display are extra fast refreshes beyond fastPerFull?
|
||||
|
||||
private:
|
||||
int32_t runOnce() override;
|
||||
void beginMaintenance(); // Excessive debt: begin unprovoked refreshing of display, for health
|
||||
int32_t endMaintenance(); // End unprovoked refreshing: debt paid
|
||||
|
||||
float debt = 0.0; // How many full refreshes are due
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,177 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Singleton class, which manages the broadest InkHUD behaviors
|
||||
|
||||
Tasks include:
|
||||
- containing instances of Tiles and Applets
|
||||
- co-ordinating display updates
|
||||
- interacting with other NicheGraphics componets, such as the driver, and input sources
|
||||
- handling system-wide events (e.g. shutdown)
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "main.h"
|
||||
#include "modules/TextMessageModule.h"
|
||||
#include "power.h"
|
||||
#include "sleep.h"
|
||||
|
||||
#include "./Applet.h"
|
||||
#include "./Applets/System/Notification/Notification.h"
|
||||
#include "./Persistence.h"
|
||||
#include "./Tile.h"
|
||||
#include "./Types.h"
|
||||
#include "./UpdateMediator.h"
|
||||
#include "graphics/niche/Drivers/EInk/EInk.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class Applet;
|
||||
class Tile;
|
||||
|
||||
class LogoApplet;
|
||||
class MenuApplet;
|
||||
class NotificationApplet;
|
||||
|
||||
class WindowManager : protected concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
static WindowManager *getInstance(); // Get or create singleton instance
|
||||
|
||||
void setDriver(NicheGraphics::Drivers::EInk *driver); // Assign a driver class
|
||||
void setDisplayResilience(uint8_t fastPerFull, float stressMultiplier); // How many FAST updates before FULL
|
||||
void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false,
|
||||
uint8_t onTile = -1); // Select which applets are used with InkHUD
|
||||
void begin(); // Start running the window manager (provisioning done)
|
||||
|
||||
void createSystemApplets(); // Instantiate and activate system applets
|
||||
void createSystemTiles(); // Instantiate tiles which host system applets
|
||||
void assignSystemAppletsToTiles();
|
||||
void placeSystemTiles(); // Set position and size
|
||||
void claimFullscreen(Applet *sa); // Assign a system applet to the fullscreen tile
|
||||
void releaseFullscreen(); // Remove any system applet from the fullscreen tile
|
||||
|
||||
void createUserApplets(); // Activate user's selected applets
|
||||
void createUserTiles(); // Instantiate enough tiles for user's selected layout
|
||||
void assignUserAppletsToTiles();
|
||||
void placeUserTiles(); // Automatically place tiles, according to user's layout
|
||||
void refocusTile(); // Ensure focused tile has a valid applet
|
||||
|
||||
int beforeDeepSleep(void *unused); // Prepare for shutdown
|
||||
int beforeReboot(void *unused); // Prepare for reboot
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message
|
||||
#ifdef ARCH_ESP32
|
||||
int beforeLightSleep(void *unused); // Prepare for light sleep
|
||||
#endif
|
||||
|
||||
void handleButtonShort(); // User button: short press
|
||||
void handleButtonLong(); // User button: long press
|
||||
|
||||
void nextApplet(); // Cycle through user applets
|
||||
void nextTile(); // Focus the next tile (when showing multiple applets at once)
|
||||
|
||||
void changeLayout(); // Change tile layout or count
|
||||
void changeActivatedApplets(); // Change which applets are activated
|
||||
void toggleBatteryIcon(); // Change whether the battery icon is shown
|
||||
bool approveNotification(Notification &n); // Ask applets if a notification is worth showing
|
||||
|
||||
void handleTilePixel(int16_t x, int16_t y, Color c); // Apply rotation, then store the pixel in framebuffer
|
||||
void requestUpdate(); // Update display, if a foreground applet has info it wants to show
|
||||
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED,
|
||||
bool async = true); // Update display, regardless of whether any applets requested this
|
||||
|
||||
uint16_t getWidth(); // Display width, relative to rotation
|
||||
uint16_t getHeight(); // Display height, relative to rotation
|
||||
uint8_t getAppletCount(); // How many user applets are available, including inactivated
|
||||
const char *getAppletName(uint8_t index); // By order in userApplets
|
||||
|
||||
void lock(Applet *owner); // Allows system applets to prevent other applets triggering a refresh
|
||||
void unlock(Applet *owner); // Allows normal updating of user applets to continue
|
||||
bool canRequestUpdate(Applet *a = nullptr); // Checks if allowed to request an update (not locked by other applet)
|
||||
Applet *whoLocked(); // Find which applet is blocking update requests, if any
|
||||
|
||||
protected:
|
||||
WindowManager(); // Private constructor for singleton
|
||||
|
||||
int32_t runOnce() override;
|
||||
|
||||
void clearBuffer(); // Empty the framebuffer
|
||||
void autoshow(); // Show a different applet, to display new info
|
||||
bool shouldUpdate(); // Check if reason to change display image
|
||||
Drivers::EInk::UpdateTypes selectUpdateType(); // Determine how the display hardware will perform the image update
|
||||
void renderUserApplets(); // Draw all currently displayed user applets to the frame buffer
|
||||
void renderSystemApplets(); // Draw all currently displayed system applets to the frame buffer
|
||||
void renderPlaceholders(); // Draw diagonal lines on user tiles which have no assigned applet
|
||||
void render(bool async = true); // Attempt to update the display
|
||||
|
||||
void setBufferPixel(int16_t x, int16_t y, Color c); // Place pixels into the frame buffer. All translation / rotation done.
|
||||
void rotatePixelCoords(int16_t *x, int16_t *y); // Apply the display rotation
|
||||
|
||||
void findOrphanApplets(); // Find any applets left-behind when layout changes
|
||||
|
||||
// Get notified when the system is shutting down
|
||||
CallbackObserver<WindowManager, void *> deepSleepObserver =
|
||||
CallbackObserver<WindowManager, void *>(this, &WindowManager::beforeDeepSleep);
|
||||
|
||||
// Get notified when the system is rebooting
|
||||
CallbackObserver<WindowManager, void *> rebootObserver =
|
||||
CallbackObserver<WindowManager, void *>(this, &WindowManager::beforeReboot);
|
||||
|
||||
// Cache *incoming* text messages, for use by applets
|
||||
CallbackObserver<WindowManager, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<WindowManager, const meshtastic_MeshPacket *>(this, &WindowManager::onReceiveTextMessage);
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Get notified when the system is entering light sleep
|
||||
CallbackObserver<WindowManager, void *> lightSleepObserver =
|
||||
CallbackObserver<WindowManager, void *>(this, &WindowManager::beforeLightSleep);
|
||||
#endif
|
||||
|
||||
NicheGraphics::Drivers::EInk *driver = nullptr;
|
||||
uint8_t *imageBuffer; // Fed into driver
|
||||
uint16_t imageBufferHeight;
|
||||
uint16_t imageBufferWidth;
|
||||
uint32_t imageBufferSize; // Bytes
|
||||
|
||||
// Encapsulates decision making about E-Ink update types
|
||||
// Responsible for display health
|
||||
UpdateMediator mediator;
|
||||
|
||||
// User Applets
|
||||
std::vector<Applet *> userApplets;
|
||||
std::vector<Tile *> userTiles;
|
||||
|
||||
// System Applets
|
||||
std::vector<Applet *> systemApplets;
|
||||
Tile *fullscreenTile = nullptr;
|
||||
Tile *notificationTile = nullptr;
|
||||
Tile *batteryIconTile = nullptr;
|
||||
LogoApplet *logoApplet;
|
||||
Applet *pairingApplet;
|
||||
Applet *tipsApplet;
|
||||
NotificationApplet *notificationApplet;
|
||||
Applet *batteryIconApplet;
|
||||
MenuApplet *menuApplet;
|
||||
Applet *placeholderApplet;
|
||||
|
||||
// requestUpdate
|
||||
bool requestingUpdate = false; // WindowManager::render run pending
|
||||
|
||||
// forceUpdate
|
||||
bool forcingUpdate = false; // WindowManager::render run pending, guaranteed no skip of update
|
||||
Drivers::EInk::UpdateTypes forcedUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // guaranteed update using this type
|
||||
|
||||
Applet *lockOwner = nullptr; // Which system applet (if any) is preventing other applets from requesting update
|
||||
};
|
||||
|
||||
}; // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
|
@ -0,0 +1,7 @@
|
|||
# NiceGraphics - Inputs
|
||||
|
||||
General purpose input sources, for use with NicheGraphics UIs.
|
||||
|
||||
By remaining independent, we can have tailored input sources with further complicating the code in ButtonThread and the canned messages module.
|
||||
|
||||
Depending on its role, a NicheGraphics UI may or may not want to make use of the existing input broker.
|
|
@ -0,0 +1,272 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include "./TwoButton.h"
|
||||
|
||||
#include "PowerFSM.h"
|
||||
#include "sleep.h"
|
||||
|
||||
using namespace NicheGraphics::Inputs;
|
||||
|
||||
TwoButton::TwoButton() : concurrency::OSThread("TwoButton")
|
||||
{
|
||||
// Don't start polling buttons for release immediately
|
||||
// Assume they are in a "released" state at boot
|
||||
OSThread::disable();
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Register callbacks for before and after lightsleep
|
||||
lsObserver.observe(¬ifyLightSleep);
|
||||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Get access to (or create) the singleton instance of this class
|
||||
// Accessible inside the ISRs, even though we maybe shouldn't
|
||||
TwoButton *TwoButton::getInstance()
|
||||
{
|
||||
// Instantiate the class the first time this method is called
|
||||
static TwoButton *const singletonInstance = new TwoButton;
|
||||
|
||||
return singletonInstance;
|
||||
}
|
||||
|
||||
// Begin receiving button input
|
||||
// We probably need to do this after sleep, as well as at boot
|
||||
void TwoButton::start()
|
||||
{
|
||||
if (buttons[0].pin != 0xFF)
|
||||
attachInterrupt(buttons[0].pin, TwoButton::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING);
|
||||
|
||||
if (buttons[1].pin != 0xFF)
|
||||
attachInterrupt(buttons[1].pin, TwoButton::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING);
|
||||
}
|
||||
|
||||
// Stop receiving button input, and run custom sleep code
|
||||
// Called before device sleeps. This might be power-off, or just ESP32 light sleep
|
||||
// Some devices will want to attach interrupts here, for the user button to wake from sleep
|
||||
void TwoButton::stop()
|
||||
{
|
||||
if (buttons[0].pin != 0xFF)
|
||||
detachInterrupt(buttons[0].pin);
|
||||
|
||||
if (buttons[1].pin != 0xFF)
|
||||
detachInterrupt(buttons[1].pin);
|
||||
}
|
||||
|
||||
// Configures the wiring and logic of either button
|
||||
// Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp
|
||||
void TwoButton::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].pin = pin;
|
||||
buttons[whichButton].activeLogic = LOW;
|
||||
buttons[whichButton].mode = internalPullup ? INPUT_PULLUP : INPUT; // fix me
|
||||
|
||||
pinMode(buttons[whichButton].pin, buttons[whichButton].mode);
|
||||
}
|
||||
|
||||
void TwoButton::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].debounceLength = debounceMs;
|
||||
buttons[whichButton].longpressLength = longpressMs;
|
||||
}
|
||||
|
||||
// Set what should happen when a button becomes pressed
|
||||
// Use this to implement a "while held" behavior
|
||||
void TwoButton::setHandlerDown(uint8_t whichButton, Callback onDown)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onDown = onDown;
|
||||
}
|
||||
|
||||
// Set what should happen when a button becomes unpressed
|
||||
// Use this to implement a "While held" behavior
|
||||
void TwoButton::setHandlerUp(uint8_t whichButton, Callback onUp)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onUp = onUp;
|
||||
}
|
||||
|
||||
// Set what should happen when a "short press" event has occurred
|
||||
void TwoButton::setHandlerShortPress(uint8_t whichButton, Callback onShortPress)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onShortPress = onShortPress;
|
||||
}
|
||||
|
||||
// Set what should happen when a "long press" event has fired
|
||||
// Note: this will occur while the button is still held
|
||||
void TwoButton::setHandlerLongPress(uint8_t whichButton, Callback onLongPress)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onLongPress = onLongPress;
|
||||
}
|
||||
|
||||
// Handle the start of a press to the primary button
|
||||
// Wakes our button thread
|
||||
void TwoButton::isrPrimary()
|
||||
{
|
||||
static volatile bool isrRunning = false;
|
||||
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButton *b = TwoButton::getInstance();
|
||||
if (b->buttons[0].state == State::REST) {
|
||||
b->buttons[0].state = State::IRQ;
|
||||
b->buttons[0].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the start of a press to the secondary button
|
||||
// Wakes our button thread
|
||||
void TwoButton::isrSecondary()
|
||||
{
|
||||
static volatile bool isrRunning = false;
|
||||
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButton *b = TwoButton::getInstance();
|
||||
if (b->buttons[1].state == State::REST) {
|
||||
b->buttons[1].state = State::IRQ;
|
||||
b->buttons[1].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Concise method to start our button thread
|
||||
// Follows an ISR, listening for button release
|
||||
void TwoButton::startThread()
|
||||
{
|
||||
if (!OSThread::enabled) {
|
||||
OSThread::setInterval(50);
|
||||
OSThread::enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Concise method to stop our button thread
|
||||
// Called when we no longer need to poll for button release
|
||||
void TwoButton::stopThread()
|
||||
{
|
||||
if (OSThread::enabled) {
|
||||
OSThread::disable();
|
||||
}
|
||||
|
||||
// Reset both buttons manually
|
||||
// Just in case an IRQ fires during the process of resetting the system
|
||||
// Can occur with super rapid presses?
|
||||
buttons[0].state = REST;
|
||||
buttons[1].state = REST;
|
||||
}
|
||||
|
||||
// Our button thread
|
||||
// Started by an IRQ, on either button
|
||||
// Polls for button releases
|
||||
// Stops when both buttons released
|
||||
int32_t TwoButton::runOnce()
|
||||
{
|
||||
constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button);
|
||||
|
||||
// Allow either button to request that our thread should continue polling
|
||||
bool awaitingRelease = false;
|
||||
|
||||
// Check both primary and secondary buttons
|
||||
for (uint8_t i = 0; i < BUTTON_COUNT; i++) {
|
||||
switch (buttons[i].state) {
|
||||
// No action: button has not been pressed
|
||||
case REST:
|
||||
break;
|
||||
|
||||
// New press detected by interrupt
|
||||
case IRQ:
|
||||
powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer)
|
||||
buttons[i].onDown(); // Inform that press has begun (possible hold behavior)
|
||||
buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled
|
||||
awaitingRelease = true; // Mark that polling-for-release should continue
|
||||
break;
|
||||
|
||||
// An existing press continues
|
||||
// Not held long enough to register as longpress
|
||||
case POLLING_UNFIRED: {
|
||||
uint32_t length = millis() - buttons[i].irqAtMillis;
|
||||
|
||||
// If button released since last thread tick,
|
||||
if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
|
||||
buttons[i].onUp(); // Inform that press has ended (possible release of a hold)
|
||||
buttons[i].state = State::REST; // Mark that the button has reset
|
||||
if (length > buttons[i].debounceLength && length < buttons[i].longpressLength)
|
||||
buttons[i].onShortPress();
|
||||
}
|
||||
|
||||
// If button not yet released
|
||||
else {
|
||||
awaitingRelease = true; // Mark that polling-for-release should continue
|
||||
if (length >= buttons[i].longpressLength) {
|
||||
// Raise a long press event, once
|
||||
// Then continue waiting for release, to rearm
|
||||
buttons[i].state = State::POLLING_FIRED;
|
||||
buttons[i].onLongPress();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Button still held, but duration long enough that longpress event already fired
|
||||
// Just waiting for release
|
||||
case POLLING_FIRED:
|
||||
// Release detected
|
||||
if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
|
||||
buttons[i].state = State::REST;
|
||||
buttons[i].onUp(); // Possible release of hold (in this case: *after* longpress has fired)
|
||||
}
|
||||
// Not yet released, keep polling
|
||||
else
|
||||
awaitingRelease = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If both buttons are now released
|
||||
// we don't need to waste cpu resources polling
|
||||
// IRQ will restart this thread when we next need it
|
||||
if (!awaitingRelease)
|
||||
stopThread();
|
||||
|
||||
// Run this method again, or don't..
|
||||
// Use whatever behavior was previously set by stopThread() or startThread()
|
||||
return OSThread::interval;
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
|
||||
// Detach our class' interrupts before lightsleep
|
||||
// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press
|
||||
int TwoButton::beforeLightSleep(void *unused)
|
||||
{
|
||||
stop();
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
// Reconfigure our interrupts
|
||||
// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep
|
||||
int TwoButton::afterLightSleep(esp_sleep_wakeup_cause_t cause)
|
||||
{
|
||||
start();
|
||||
|
||||
// Manually trigger the button-down ISR
|
||||
// - during light sleep, our ISR is disabled
|
||||
// - if light sleep ends by button press, pretend our own ISR caught it
|
||||
if (cause == ESP_SLEEP_WAKEUP_GPIO)
|
||||
isrPrimary();
|
||||
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
|
@ -0,0 +1,103 @@
|
|||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
/*
|
||||
|
||||
Re-usable NicheGraphics input source
|
||||
|
||||
Short and Long press for up to two buttons
|
||||
Interrupt driven
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "assert.h"
|
||||
#include "functional"
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
#include "esp_sleep.h" // For light-sleep handling
|
||||
#endif
|
||||
|
||||
#include "Observer.h"
|
||||
|
||||
namespace NicheGraphics::Inputs
|
||||
{
|
||||
|
||||
class TwoButton : protected concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
typedef std::function<void()> Callback;
|
||||
|
||||
static TwoButton *getInstance(); // Create or get the singleton instance
|
||||
void start(); // Start handling button input
|
||||
void stop(); // Stop handling button input (disconnect ISRs for sleep)
|
||||
void setWiring(uint8_t whichButton, uint8_t pin, bool internalPulldown = false);
|
||||
void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs);
|
||||
void setHandlerDown(uint8_t whichButton, Callback onDown);
|
||||
void setHandlerUp(uint8_t whichButton, Callback onUp);
|
||||
void setHandlerShortPress(uint8_t whichButton, Callback onShortPress);
|
||||
void setHandlerLongPress(uint8_t whichButton, Callback onLongPress);
|
||||
|
||||
// Disconnect and reconnect interrupts for light sleep
|
||||
#ifdef ARCH_ESP32
|
||||
int beforeLightSleep(void *unused);
|
||||
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
|
||||
#endif
|
||||
|
||||
private:
|
||||
// Internal state of a specific button
|
||||
enum State {
|
||||
REST, // Up, no activity
|
||||
IRQ, // Down detected, not yet handled
|
||||
POLLING_UNFIRED, // Down handled, polling for release
|
||||
POLLING_FIRED, // Longpress fired, button still held
|
||||
};
|
||||
|
||||
// Contains info about a specific button
|
||||
// (Array of this struct below)
|
||||
class Button
|
||||
{
|
||||
public:
|
||||
// Per-button config
|
||||
uint8_t pin = 0xFF; // 0xFF: unset
|
||||
bool activeLogic = LOW; // Active LOW by default. Todo: remove, unused
|
||||
uint8_t mode = INPUT; // Whether to use internal pull up / pull down resistors
|
||||
uint32_t debounceLength = 50; // Minimum length for shortpress, in ms
|
||||
uint32_t longpressLength = 500; // How long after button down to fire longpress, in ms
|
||||
volatile State state = State::REST; // Internal state
|
||||
volatile uint32_t irqAtMillis; // millis() when button went down
|
||||
|
||||
// Per-button event callbacks
|
||||
static void noop(){};
|
||||
std::function<void()> onDown = noop;
|
||||
std::function<void()> onUp = noop;
|
||||
std::function<void()> onShortPress = noop;
|
||||
std::function<void()> onLongPress = noop;
|
||||
};
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Get notified when lightsleep begins and ends
|
||||
CallbackObserver<TwoButton, void *> lsObserver = CallbackObserver<TwoButton, void *>(this, &TwoButton::beforeLightSleep);
|
||||
CallbackObserver<TwoButton, esp_sleep_wakeup_cause_t> lsEndObserver =
|
||||
CallbackObserver<TwoButton, esp_sleep_wakeup_cause_t>(this, &TwoButton::afterLightSleep);
|
||||
#endif
|
||||
|
||||
int32_t runOnce() override; // Timer method. Polls for button release
|
||||
|
||||
void startThread(); // Start polling for release
|
||||
void stopThread(); // Stop polling for release
|
||||
|
||||
static void isrPrimary(); // Detect start of press
|
||||
static void isrSecondary(); // Detect start of press (optional aux button)
|
||||
|
||||
TwoButton(); // Constructor made private: force use of Button::instance()
|
||||
|
||||
// Info about both buttons
|
||||
Button buttons[2];
|
||||
};
|
||||
|
||||
}; // namespace NicheGraphics::Inputs
|
||||
|
||||
#endif
|
|
@ -0,0 +1,15 @@
|
|||
# NicheGraphics
|
||||
|
||||
A pattern / collection of resources for creating custom UIs, to target small groups of devices which have specific design requirements.
|
||||
|
||||
For an example, see the `heltec-vision-master-e290-inkhud` platformio env.
|
||||
|
||||
- platformio.ini
|
||||
|
||||
- suppress default Meshtastic components (Screen, ButtonThread, etc)
|
||||
- define `MESHTASTIC_INCLUDE_NICHE_GRAPHICS`
|
||||
- (possibly) Edit `build_src_filter` to include our new nicheGraphics.h file
|
||||
|
||||
- nicheGraphics.h
|
||||
- `#include` all necessary components
|
||||
- perform all setup and config inside a `setupNicheGraphics()` method
|
|
@ -0,0 +1,126 @@
|
|||
#if HAS_TFT
|
||||
|
||||
#include "SPILock.h"
|
||||
#include "sleep.h"
|
||||
|
||||
#include "api/PacketAPI.h"
|
||||
#include "comms/PacketClient.h"
|
||||
#include "comms/PacketServer.h"
|
||||
#include "graphics/DeviceScreen.h"
|
||||
#include "graphics/driver/DisplayDriverConfig.h"
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
#include "PortduinoGlue.h"
|
||||
#endif
|
||||
|
||||
DeviceScreen *deviceScreen = nullptr;
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Get notified when the system is entering light sleep
|
||||
CallbackObserver<DeviceScreen, void *> tftSleepObserver =
|
||||
CallbackObserver<DeviceScreen, void *>(deviceScreen, &DeviceScreen::prepareSleep);
|
||||
CallbackObserver<DeviceScreen, esp_sleep_wakeup_cause_t> endSleepObserver =
|
||||
CallbackObserver<DeviceScreen, esp_sleep_wakeup_cause_t>(deviceScreen, &DeviceScreen::wakeUp);
|
||||
#endif
|
||||
|
||||
void tft_task_handler(void *param = nullptr)
|
||||
{
|
||||
while (true) {
|
||||
if (deviceScreen) {
|
||||
spiLock->lock();
|
||||
deviceScreen->task_handler();
|
||||
spiLock->unlock();
|
||||
deviceScreen->sleep();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void tftSetup(void)
|
||||
{
|
||||
#ifndef ARCH_PORTDUINO
|
||||
deviceScreen = &DeviceScreen::create();
|
||||
PacketAPI::create(PacketServer::init());
|
||||
deviceScreen->init(new PacketClient);
|
||||
#else
|
||||
if (settingsMap[displayPanel] != no_screen) {
|
||||
DisplayDriverConfig displayConfig;
|
||||
static char *panels[] = {"NOSCREEN", "X11", "ST7789", "ST7735", "ST7735S", "ST7796",
|
||||
"ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"};
|
||||
static char *touch[] = {"NOTOUCH", "XPT2046", "STMPE610", "GT911", "FT5x06"};
|
||||
#ifdef USE_X11
|
||||
if (settingsMap[displayPanel] == x11) {
|
||||
if (settingsMap[displayWidth] && settingsMap[displayHeight])
|
||||
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)settingsMap[displayWidth],
|
||||
(uint16_t)settingsMap[displayHeight]);
|
||||
else
|
||||
displayConfig.device(DisplayDriverConfig::device_t::X11);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
displayConfig.device(DisplayDriverConfig::device_t::CUSTOM_TFT)
|
||||
.panel(DisplayDriverConfig::panel_config_t{.type = panels[settingsMap[displayPanel]],
|
||||
.panel_width = (uint16_t)settingsMap[displayWidth],
|
||||
.panel_height = (uint16_t)settingsMap[displayHeight],
|
||||
.rotation = (bool)settingsMap[displayRotate],
|
||||
.pin_cs = (int16_t)settingsMap[displayCS],
|
||||
.pin_rst = (int16_t)settingsMap[displayReset],
|
||||
.offset_x = (uint16_t)settingsMap[displayOffsetX],
|
||||
.offset_y = (uint16_t)settingsMap[displayOffsetY],
|
||||
.offset_rotation = (uint8_t)settingsMap[displayOffsetRotate],
|
||||
.invert = settingsMap[displayInvert] ? true : false,
|
||||
.rgb_order = (bool)settingsMap[displayRGBOrder],
|
||||
.dlen_16bit = settingsMap[displayPanel] == ili9486 ||
|
||||
settingsMap[displayPanel] == ili9488})
|
||||
.bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)settingsMap[displayBusFrequency],
|
||||
.freq_read = 16000000,
|
||||
.spi{.pin_dc = (int8_t)settingsMap[displayDC],
|
||||
.use_lock = true,
|
||||
.spi_host = (uint16_t)settingsMap[displayspidev]}})
|
||||
.input(DisplayDriverConfig::input_config_t{.keyboardDevice = settingsStrings[keyboardDevice],
|
||||
.pointerDevice = settingsStrings[pointerDevice]})
|
||||
.light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)settingsMap[displayBacklight],
|
||||
.pwm_channel = (int8_t)settingsMap[displayBacklightPWMChannel],
|
||||
.invert = (bool)settingsMap[displayBacklightInvert]});
|
||||
if (settingsMap[touchscreenI2CAddr] == -1) {
|
||||
displayConfig.touch(
|
||||
DisplayDriverConfig::touch_config_t{.type = touch[settingsMap[touchscreenModule]],
|
||||
.freq = (uint32_t)settingsMap[touchscreenBusFrequency],
|
||||
.pin_int = (int16_t)settingsMap[touchscreenIRQ],
|
||||
.offset_rotation = (uint8_t)settingsMap[touchscreenRotate],
|
||||
.spi{
|
||||
.spi_host = (int8_t)settingsMap[touchscreenspidev],
|
||||
},
|
||||
.pin_cs = (int16_t)settingsMap[touchscreenCS]});
|
||||
} else {
|
||||
displayConfig.touch(DisplayDriverConfig::touch_config_t{
|
||||
.type = touch[settingsMap[touchscreenModule]],
|
||||
.freq = (uint32_t)settingsMap[touchscreenBusFrequency],
|
||||
.x_min = 0,
|
||||
.x_max =
|
||||
(int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayWidth] : settingsMap[displayHeight]) -
|
||||
1),
|
||||
.y_min = 0,
|
||||
.y_max =
|
||||
(int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayHeight] : settingsMap[displayWidth]) -
|
||||
1),
|
||||
.pin_int = (int16_t)settingsMap[touchscreenIRQ],
|
||||
.offset_rotation = (uint8_t)settingsMap[touchscreenRotate],
|
||||
.i2c{.i2c_addr = (uint8_t)settingsMap[touchscreenI2CAddr]}});
|
||||
}
|
||||
}
|
||||
deviceScreen = &DeviceScreen::create(&displayConfig);
|
||||
PacketAPI::create(PacketServer::init());
|
||||
deviceScreen->init(new PacketClient);
|
||||
} else {
|
||||
LOG_INFO("Running without TFT display!");
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
tftSleepObserver.observe(¬ifyLightSleep);
|
||||
endSleepObserver.observe(¬ifyLightSleepEnd);
|
||||
xTaskCreatePinnedToCore(tft_task_handler, "tft", 8192, NULL, 1, NULL, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
62
src/main.cpp
62
src/main.cpp
|
@ -115,6 +115,24 @@ AccelerometerThread *accelerometerThread = nullptr;
|
|||
AudioThread *audioThread = nullptr;
|
||||
#endif
|
||||
|
||||
#if HAS_TFT
|
||||
extern void tftSetup(void);
|
||||
#endif
|
||||
|
||||
#ifdef HAS_UDP_MULTICAST
|
||||
#include "mesh/udp/UdpMulticastThread.h"
|
||||
UdpMulticastThread *udpThread = nullptr;
|
||||
#endif
|
||||
|
||||
#if defined(TCXO_OPTIONAL)
|
||||
float tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; // if TCXO is optional, put this here so it can be changed further down.
|
||||
#endif
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
void setupNicheGraphics();
|
||||
#include "nicheGraphics.h"
|
||||
#endif
|
||||
|
||||
using namespace concurrency;
|
||||
|
||||
volatile static const char slipstreamTZString[] = {USERPREFS_TZ_STRING};
|
||||
|
@ -131,6 +149,9 @@ meshtastic::GPSStatus *gpsStatus = new meshtastic::GPSStatus();
|
|||
// Global Node status
|
||||
meshtastic::NodeStatus *nodeStatus = new meshtastic::NodeStatus();
|
||||
|
||||
// Global Bluetooth status
|
||||
meshtastic::BluetoothStatus *bluetoothStatus = new meshtastic::BluetoothStatus();
|
||||
|
||||
// Scan for I2C Devices
|
||||
|
||||
/// The I2C address of our display (if found)
|
||||
|
@ -249,6 +270,15 @@ void setup()
|
|||
// TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder)
|
||||
pinMode(KB_POWERON, OUTPUT);
|
||||
digitalWrite(KB_POWERON, HIGH);
|
||||
// T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus
|
||||
// We need to initialize all CS pins in advance otherwise there will be SPI communication issues
|
||||
// e.g. when detecting the SD card
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(TFT_CS, OUTPUT);
|
||||
digitalWrite(TFT_CS, HIGH);
|
||||
delay(100);
|
||||
#endif
|
||||
|
||||
|
@ -426,6 +456,10 @@ void setup()
|
|||
digitalWrite(AQ_SET_PIN, HIGH);
|
||||
#endif
|
||||
|
||||
#if HAS_TFT
|
||||
tftSetup();
|
||||
#endif
|
||||
|
||||
// Currently only the tbeam has a PMU
|
||||
// PMU initialization needs to be placed before i2c scanning
|
||||
power = new Power();
|
||||
|
@ -644,9 +678,9 @@ void setup()
|
|||
// but we need to do this after main cpu init (esp32setup), because we need the random seed set
|
||||
nodeDB = new NodeDB;
|
||||
|
||||
// If we're taking on the repeater role, use flood router and turn off 3V3_S rail because peripherals are not needed
|
||||
// If we're taking on the repeater role, use NextHopRouter and turn off 3V3_S rail because peripherals are not needed
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) {
|
||||
router = new FloodingRouter();
|
||||
router = new NextHopRouter();
|
||||
#ifdef PIN_3V3_EN
|
||||
digitalWrite(PIN_3V3_EN, LOW);
|
||||
#endif
|
||||
|
@ -731,8 +765,9 @@ void setup()
|
|||
#endif
|
||||
|
||||
// Initialize the screen first so we can show the logo while we start up everything else.
|
||||
#if HAS_SCREEN
|
||||
screen = new graphics::Screen(screen_found, screen_model, screen_geometry);
|
||||
|
||||
#endif
|
||||
// setup TZ prior to time actions.
|
||||
#if !MESHTASTIC_EXCLUDE_TZ
|
||||
LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string
|
||||
|
@ -781,12 +816,22 @@ void setup()
|
|||
LOG_DEBUG("Start audio thread");
|
||||
audioThread = new AudioThread();
|
||||
#endif
|
||||
|
||||
#ifdef HAS_UDP_MULTICAST
|
||||
LOG_DEBUG("Start multicast thread");
|
||||
udpThread = new UdpMulticastThread();
|
||||
#endif
|
||||
service = new MeshService();
|
||||
service->init();
|
||||
|
||||
// Now that the mesh service is created, create any modules
|
||||
setupModules();
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
// After modules are setup, so we can observe modules
|
||||
setupNicheGraphics();
|
||||
#endif
|
||||
|
||||
#ifdef LED_PIN
|
||||
// Turn LED off after boot, if heartbeat by config
|
||||
if (config.device.led_heartbeat_disabled)
|
||||
|
@ -1124,7 +1169,15 @@ void setup()
|
|||
// This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values
|
||||
PowerFSM_setup(); // we will transition to ON in a couple of seconds, FIXME, only do this for cold boots, not waking from SDS
|
||||
powerFSMthread = new PowerFSMThread();
|
||||
|
||||
#if !HAS_TFT
|
||||
setCPUFast(false); // 80MHz is fine for our slow peripherals
|
||||
#endif
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
LOG_DEBUG("Free heap : %7d bytes", ESP.getFreeHeap());
|
||||
LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram());
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
uint32_t rebootAtMsec; // If not zero we will reboot at this time (used to reboot shortly after the update completes)
|
||||
|
@ -1221,4 +1274,5 @@ void loop()
|
|||
mainDelay.delay(delayMsec);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include "BluetoothStatus.h"
|
||||
#include "GPSStatus.h"
|
||||
#include "NodeStatus.h"
|
||||
#include "PowerStatus.h"
|
||||
|
@ -49,6 +50,11 @@ extern Adafruit_DRV2605 drv;
|
|||
extern AudioThread *audioThread;
|
||||
#endif
|
||||
|
||||
#ifdef HAS_UDP_MULTICAST
|
||||
#include "mesh/udp/UdpMulticastThread.h"
|
||||
extern UdpMulticastThread *udpThread;
|
||||
#endif
|
||||
|
||||
// Global Screen singleton.
|
||||
extern graphics::Screen *screen;
|
||||
|
||||
|
|
|
@ -93,6 +93,35 @@ void Channels::initDefaultLoraConfig()
|
|||
#endif
|
||||
}
|
||||
|
||||
bool Channels::ensureLicensedOperation()
|
||||
{
|
||||
if (!owner.is_licensed) {
|
||||
return false;
|
||||
}
|
||||
bool hasEncryptionOrAdmin = false;
|
||||
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
|
||||
auto channel = channels.getByIndex(i);
|
||||
if (!channel.has_settings) {
|
||||
continue;
|
||||
}
|
||||
auto &channelSettings = channel.settings;
|
||||
if (strcasecmp(channelSettings.name, Channels::adminChannel) == 0) {
|
||||
channel.role = meshtastic_Channel_Role_DISABLED;
|
||||
channelSettings.psk.bytes[0] = 0;
|
||||
channelSettings.psk.size = 0;
|
||||
hasEncryptionOrAdmin = true;
|
||||
channels.setChannel(channel);
|
||||
|
||||
} else if (channelSettings.psk.size > 0) {
|
||||
channelSettings.psk.bytes[0] = 0;
|
||||
channelSettings.psk.size = 0;
|
||||
hasEncryptionOrAdmin = true;
|
||||
channels.setChannel(channel);
|
||||
}
|
||||
}
|
||||
return hasEncryptionOrAdmin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a default channel to the specified channel index
|
||||
*/
|
||||
|
|
|
@ -92,6 +92,8 @@ class Channels
|
|||
// Returns true if any of our channels have enabled MQTT uplink or downlink
|
||||
bool anyMqttEnabled();
|
||||
|
||||
bool ensureLicensedOperation();
|
||||
|
||||
private:
|
||||
/** Given a channel index, change to use the crypto key specified by that index
|
||||
*
|
||||
|
|
|
@ -13,7 +13,8 @@ FloodingRouter::FloodingRouter() {}
|
|||
ErrorCode FloodingRouter::send(meshtastic_MeshPacket *p)
|
||||
{
|
||||
// Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see)
|
||||
wasSeenRecently(p); // FIXME, move this to a sniffSent method
|
||||
p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us
|
||||
wasSeenRecently(p); // FIXME, move this to a sniffSent method
|
||||
|
||||
return Router::send(p);
|
||||
}
|
||||
|
@ -23,26 +24,17 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
|
|||
if (wasSeenRecently(p)) { // Note: this will also add a recent packet record
|
||||
printPacket("Ignore dupe incoming msg", p);
|
||||
rxDupe++;
|
||||
if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER &&
|
||||
config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER &&
|
||||
config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) {
|
||||
// cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater!
|
||||
if (Router::cancelSending(p->from, p->id))
|
||||
txRelayCanceled++;
|
||||
}
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) {
|
||||
iface->clampToLateRebroadcastWindow(getFrom(p), p->id);
|
||||
}
|
||||
|
||||
/* If the original transmitter is doing retransmissions (hopStart equals hopLimit) for a reliable transmission, e.g., when
|
||||
the ACK got lost, we will handle the packet again to make sure it gets an ACK to its packet. */
|
||||
the ACK got lost, we will handle the packet again to make sure it gets an implicit ACK. */
|
||||
bool isRepeated = p->hop_start > 0 && p->hop_start == p->hop_limit;
|
||||
if (isRepeated) {
|
||||
LOG_DEBUG("Repeated reliable tx");
|
||||
if (!perhapsRebroadcast(p) && isToUs(p) && p->want_ack) {
|
||||
// FIXME - channel index should be used, but the packet is still encrypted here
|
||||
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, 0, 0);
|
||||
}
|
||||
// Check if it's still in the Tx queue, if not, we have to relay it again
|
||||
if (!findInTxQueue(p->from, p->id))
|
||||
perhapsRebroadcast(p);
|
||||
} else {
|
||||
perhapsCancelDupe(p);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -51,13 +43,27 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
|
|||
return Router::shouldFilterReceived(p);
|
||||
}
|
||||
|
||||
void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER &&
|
||||
config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER &&
|
||||
config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) {
|
||||
// cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater!
|
||||
if (Router::cancelSending(p->from, p->id))
|
||||
txRelayCanceled++;
|
||||
}
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) {
|
||||
iface->clampToLateRebroadcastWindow(getFrom(p), p->id);
|
||||
}
|
||||
}
|
||||
|
||||
bool FloodingRouter::isRebroadcaster()
|
||||
{
|
||||
return config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE &&
|
||||
config.device.rebroadcast_mode != meshtastic_Config_DeviceConfig_RebroadcastMode_NONE;
|
||||
}
|
||||
|
||||
bool FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
|
||||
void FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
if (!isToUs(p) && (p->hop_limit > 0) && !isFromUs(p)) {
|
||||
if (p->id != 0) {
|
||||
|
@ -72,13 +78,12 @@ bool FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
|
|||
tosend->hop_limit = 2;
|
||||
}
|
||||
#endif
|
||||
tosend->next_hop = NO_NEXT_HOP_PREFERENCE; // this should already be the case, but just in case
|
||||
|
||||
LOG_INFO("Rebroadcast received floodmsg");
|
||||
// Note: we are careful to resend using the original senders node id
|
||||
// We are careful not to call our hooked version of send() - because we don't want to check this again
|
||||
Router::send(tosend);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
LOG_DEBUG("No rebroadcast: Role = CLIENT_MUTE or Rebroadcast Mode = NONE");
|
||||
}
|
||||
|
@ -86,13 +91,12 @@ bool FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
|
|||
LOG_DEBUG("Ignore 0 id broadcast");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void FloodingRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c)
|
||||
{
|
||||
bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && (p->decoded.request_id != 0);
|
||||
bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) &&
|
||||
(p->decoded.request_id != 0 || p->decoded.reply_id != 0);
|
||||
if (isAckorReply && !isToUs(p) && !isBroadcast(p->to)) {
|
||||
// do not flood direct message that is ACKed or replied to
|
||||
LOG_DEBUG("Rxd an ACK/reply not for me, cancel rebroadcast");
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "PacketHistory.h"
|
||||
#include "Router.h"
|
||||
|
||||
/**
|
||||
|
@ -26,14 +25,11 @@
|
|||
Any entries in recentBroadcasts that are older than X seconds (longer than the
|
||||
max time a flood can take) will be discarded.
|
||||
*/
|
||||
class FloodingRouter : public Router, protected PacketHistory
|
||||
class FloodingRouter : public Router
|
||||
{
|
||||
private:
|
||||
bool isRebroadcaster();
|
||||
|
||||
/** Check if we should rebroadcast this packet, and do so if needed
|
||||
* @return true if rebroadcasted */
|
||||
bool perhapsRebroadcast(const meshtastic_MeshPacket *p);
|
||||
/* Check if we should rebroadcast this packet, and do so if needed */
|
||||
void perhapsRebroadcast(const meshtastic_MeshPacket *p);
|
||||
|
||||
public:
|
||||
/**
|
||||
|
@ -62,4 +58,10 @@ class FloodingRouter : public Router, protected PacketHistory
|
|||
* Look for broadcasts we need to rebroadcast
|
||||
*/
|
||||
virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override;
|
||||
|
||||
/* Call when receiving a duplicate packet to check whether we should cancel a packet in the Tx queue */
|
||||
void perhapsCancelDupe(const meshtastic_MeshPacket *p);
|
||||
|
||||
// Return true if we are a rebroadcaster
|
||||
bool isRebroadcaster();
|
||||
};
|
|
@ -262,10 +262,17 @@ template <typename T> void LR11x0Interface<T>::startReceive()
|
|||
template <typename T> bool LR11x0Interface<T>::isChannelActive()
|
||||
{
|
||||
// check if we can detect a LoRa preamble on the current channel
|
||||
ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD,
|
||||
.detPeak = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT,
|
||||
.detMin = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT,
|
||||
.exitMode = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT,
|
||||
.timeout = 0,
|
||||
.irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS,
|
||||
.irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}};
|
||||
int16_t result;
|
||||
|
||||
setStandby();
|
||||
result = lora.scanChannel();
|
||||
result = lora.scanChannel(cfg);
|
||||
if (result == RADIOLIB_LORA_DETECTED)
|
||||
return true;
|
||||
|
||||
|
|
|
@ -117,6 +117,19 @@ meshtastic_MeshPacket *MeshPacketQueue::remove(NodeNum from, PacketId id, bool t
|
|||
return NULL;
|
||||
}
|
||||
|
||||
/* Attempt to find a packet from this queue. Return true if it was found. */
|
||||
bool MeshPacketQueue::find(NodeNum from, PacketId id)
|
||||
{
|
||||
for (auto it = queue.begin(); it != queue.end(); it++) {
|
||||
auto p = (*it);
|
||||
if (getFrom(p) == from && p->id == id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to find a lower-priority packet in the queue and replace it with the provided one.
|
||||
* @return True if the replacement succeeded, false otherwise
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue