mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-17 09:01:30 +10:00
+19








4c91beeda9
* Don't ever define PIN_LED or BLE_LED_INVERTED (#9494) * Don't ever define PIN_LED * Deprecate BLE_LED_INVERTED * Add StatusMessage module and config overrides (#9351) * Add StatusMessage module and config overrides * Trunk * Don't reboot node simply for a StatusMessage config update * Missed in reviews - fixing send bubble (#9505) * Prefer EXT_PWR_DETECT pin over chargingVolt to detect power unplugged (#9511) * Make sure we always return a value in NodeDB::restorePreferences() (#9516) In case FScom is not defined there is no return statement. This moves the return outside of the ifdef to make sure a defined value is returned. * Inkhud battery icon improvements. (#9513) * Inkhud battery icon improvements. Fixes the battery icon draining from the flat side towards the bump, which is backwards from general design language seen on most devices By request of kr0n05_ on discord, adds the ability to mirror the battery icon which fixes that issue in another way, and is also a common design seen on other devices. * Remove option for icon mirroring * Add border + dither to battery to prevent font overlap * Fix trunk format * Code cleanup, courtesy of Xaositek. * Add reply bot module with DM-only responses and rate limiting (#9456) * Implement Meshtastic reply bot module with ping and status features Adds a reply bot module that listens for /ping, /hello, and /test commands received via direct messages or broadcasts on the primary channel. The module always replies via direct message to the sender only, reporting hop count, RSSI, and SNR. Per-sender cooldowns are enforced to reduce network spam, and the module can be excluded at build time via a compile flag. Updates include the new module source files and required build configuration changes. * Update ReplyBotModule.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/ReplyBotModule.h Match the existing MESHTASTIC_EXCLUDE_* guard pattern so the module is excluded by default. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/ReplyBotModule.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Tidying up --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * HotFix for ReplyBot - Modules.cpp included and moved configuration.h (#9532) * Undefine LED_BUILTIN (#9531) Keep variant in sync with https://github.com/meshtastic/firmware/commit/df40085 Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Add agc reset attempt (#8163) * Add agc reset attempt * Add radioLibInterface include * Trunk * AGC reset don't crash, don't naively call * Update src/main.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Use Throttle function --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove unused hmx variable (#9529) The variable is not used at all in the function, remove it to silence the compiler warning. Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Rename LED_PIN to LED_POWER, move handling out of main to dedicated module (#9512) * Rename LED_PIN to LED_POWER, move handling out of main to dedicated module * Misc * Remove errant endif * Fix hop_limit upgrade detection (#9550) Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * meshtasticd: Fix install on Fedora 43 (#9556) * RPM: Include meshtasticd-start.sh (#9561) * Add Slash Key to VirtualKeyboard (#9563) Addition of ? and / to the virtual Keyboard via short and long press * Add support for CW2015 LiPo battery fuel gauge (#9564) * Add support for CW2015 LiPo battery fuel gauge * Address Copilot's concerns, minor fixups * Make LED_POWER blip even in critical battery (#9545) * Enable FORTIFY and SP for native builds (#9537) * Enable FORITFY and NX for native builds meshtasticd does have an executable stack and is not built with fortify, which makes exploitation of memory corruption bugs easier than it has to be. This enables fortify and a non-executable stack. This gives the following improvements on Debian Trixie: $ checksec --file=./.pio/build/native/meshtasticd RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 13516 Symbols No 0 17 ./.pio/build/native/meshtasticd $ checksec --file=./.pio/build/native/meshtasticd RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 13519 Symbols Yes 12 20 ./.pio/build/native/meshtasticd Tested with --sim mode I do not get any crashes or similar. * Enable FORTIFY and NX for native builds meshtasticd does have an executable stack and is not built with fortify, which makes exploitation of memory corruption bugs easier than it has to be. This enables fortify and a non-executable stack. This gives the following improvements on Debian Trixie: $ checksec --file=./.pio/build/native/meshtasticd RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 13516 Symbols No 0 17 ./.pio/build/native/meshtasticd $ checksec --file=./.pio/build/native/meshtasticd RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 13519 Symbols Yes 12 20 ./.pio/build/native/meshtasticd Tested with --sim mode I do not get any crashes or similar. * Enable FORTIFY and SP for native builds meshtasticd does have a stack canaries and is not built with fortify, which makes exploitation of memory corruption bugs easier than it has to be. This enables fortify and stack canaries. This gives the following improvements on Debian Trixie: $ checksec --file=./.pio/build/native/meshtasticd RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 13516 Symbols No 0 17 ./.pio/build/native/meshtasticd $ checksec --file=./.pio/build/native/meshtasticd RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 13519 Symbols Yes 12 20 ./.pio/build/native/meshtasticd Tested with --sim mode I do not get any crashes or similar. --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Update built-in documentation for current method of implementation (#9592) * Refactor logging in ProtobufModule to ensure message details are logged after successful decoding (#9536) * Automated version bumps (#9604) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Add missing openocd_target to custom nrf52 boards (#9603) This stops platformio complaining about `Missing target configuration for me25ls01-4y10td` etc when trying to flash with nrfutil. * Add sdl libs for native builds (#9595) * Add sdl libs for native builds * Alpine try again * fix some random compiler warnings (#9596) * Modify the dependency library of v4-tft (#9507) * BaseUI: Favorite Screen Signal Quality improvement (#9566) * Favorite Signal Quality improvement * Show Voltage if node shares it. * Trunk Fix * Change Favorite tittle to encase name with Asterisks * Add Pluggin In condition for Battery Line * Adjust getUptimeStr Prefixes * Create isAPIConnected for SharedCommon usage * Correct leftSideSpacing to account for isAPIConnected --------- Co-authored-by: Jason P <applewiz@mac.com> * ExternalNotification and StatusLED now call AmbientLighting to update… (#9554) * ExternalNotification and StatusLED now call AmbientLighting to update RGB LEDs. Add optional heartbeat * Don't overwrite RGB state if heartbeat is disabled. * Use the right define * Remove another .h and make rgb static * move rgb objects into AmbientLighting class * Straighten out AmbientLighting Thread object * Use %f for floats * Fixes on SCD4X admin comands (#9607) * Fixes on SCD4X admin comands * Minor fix in logs for SEN5X --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * feat/add sfa30 (#9372) * Move PMSA003I to separate class and update AQ telemetry * AirQualityTelemetry module not depend on PM sensor presence * Remove commented line * Fixes on PMS class * Add missing warmup period to wakeUp function * Fixes on compilation for different variants * Add functions to check for I2C bus speed and set it * Initial implementation for SFA30Sensor * Move PMSA003I to separate class and update AQ telemetry * AirQualityTelemetry module not depend on PM sensor presence * Remove commented line * Fixes on PMS class * Add missing warmup period to wakeUp function * Fixes on compilation for different variants * Add functions to check for I2C bus speed and set it * Add ScreenFonts.h Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de> * PMSA003I 1st round test * Fix I2C scan speed * Fix minor issues and bring back I2C SPEED def * Remove PMSA003I library as its no longer needed * Add functional SCD4X * Fix screen frame for CO2 * Add admin commands to SCD4X class * Add further admin commands and fixes. * Remove unused I2C speed functions and cleanup * Cleanup of SEN5X specific code added from switching branches * Remove SCAN_I2C_CLOCK_SPEED block as its not needed * Remove associated functions for setting I2C speed * Unify build epoch to add flag in platformio-custom.py (#7917) * Unify build_epoch replacement logic in platformio-custom * Missed one * Fix build error in rak_wismesh_tap_v2 (#7905) In the logs was: "No screen resolution defined in build_flags. Please define DISPLAY_SIZE." set according to similar devices. * Put guards in place around debug heap operations (#7955) * Put guards in place around debug heap operations * Add macros to clean up code * Add pointer as well * Cleanup * Fix memory leak in NextHopRouter: always free packet copy when removing from pending * Formatting * Only queue 2 client notification * Merge pull request #7965 from compumike/compumike/fix-nrf52-bluetooth-memory-leak Fix memory leak in `NRF52Bluetooth`: allocate `BluetoothStatus` on stack, not heap * Merge pull request #7964 from compumike/compumike/fix-nimble-bluetooth-memory-leak Fix memory leak in `NimbleBluetooth`: allocate `BluetoothStatus` on stack, not heap * Update protobufs (#7973) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * T-Lora Pager: Support LR1121 and SX1280 models (#7956) * T-Lora Pager: Support LR1121 and SX1280 models * Remove ifdefs * Trunk * Trunk * Static memory pool allocation (#7966) * Static memory pool * Initializer * T-Lora Pager: Support LR1121 and SX1280 models (#7956) * T-Lora Pager: Support LR1121 and SX1280 models * Remove ifdefs --------- Co-authored-by: WillyJL <me@willyjl.dev> * Portduino dynamic alloc * Missed * Drop the limit * Update meshtastic-esp8266-oled-ssd1306 digest to 0cbc26b (#7977) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fix json report crashes on esp32 (#7978) * Tweak maximums * Fix DRAM overflow on old esp32 targets * Guard bad time warning logs using GPS_DEBUG (#7897) In 2.7.7 / 2.7.8 we introduced some new checks for time accuracy. In combination, these result in a spamming of the logs when a bad time is found When the GPS is active, we're calling the GPS thread every 0.2secs. So this log could be printed 4,500 times in a no-lock scenario :) Reserve this experience for developers using GPS_DEBUG. Fixes https://github.com/meshtastic/firmware/issues/7896 * Scale probe buffer size based on current baud rate (#7975) * Scale probe buffer size based on current baud rate * Throttle bad time validation logging and fix time comparison logic * Remove comment * Missed the other instances * Copy pasta * Fix GPS gm_mktime memory leak (#7981) * Fix overflow of time value (#7984) * Fix overflow of time value * Revert "Fix overflow of time value" This reverts commit 084796920179e80a7500d36c25fd4d82b3ef4214. * That got boogered up * Remove PMSA003 include from modules * Add flag to exclude air quality module * Rework PMSA003I to align with new I2C scanner * Reworks AQ telemetry to match new dynamic allocation method * Adds VBLE_I2C_CLOCK_SPEED build flag for sensors with different I2C speed requirements * Reworks PMSA003I * Move add sensor template to separate file * Split telemetry on screen options * Add variable I2C clock compile flag * Added to Seeed Xiao S3 as demo * Fix drawFrame in AQ module * Module settings override to i2cScan module function * Move to CAN_RECLOCK_I2C per architecture * Add reclock function in TelemetrySensor.cpp * Add flag in ESP32 common * Minor fix * Move I2C reclock function to src/detect * Fix uninitMemberVar errors and compile issue * Make sleep, wakeUp functions generic * Fix STM32 builds * Add exclude AQ sensor to builds that have environmental sensor excludes * Add includes to AddI2CSensorTemplate.h * SEN5X first pass * WIP Sen5X functions * Further (non-working) progress in SEN5X * WIP Sen5X functions * Changes on SEN5X library - removing pm_env as well * Small cleanup of SEN5X sensors * Minor change for SEN5X detection * Remove dup code * Enable PM sensor before sending telemetry. This enables the PM sensor for a predefined period to allow for warmup. Once telemetry is sent, the sensor shuts down again. * Small cleanups in SEN5X sensor * Add dynamic measurement interval for SEN5X * Only disable SEN5X if enough time after reading. * Idle for SEN5X on communication error * Cleanup of logs and remove unnecessary delays * Small TODO * Settle on uint16_t for SEN5X PM data * Make AQTelemetry sensors non-exclusive * Implementation of cleaning in FS prefs and cleanup * Remove unnecessary LOGS * Add cleaning date storage in FS * Report non-cumulative PN * Bring back detection code for SEN5X after branch rebase * Add placeholder for admin message * Add VOC measurements and persistence (WIP) * Adds VOC measurements and state * Still not working on VOC Index persistence * Should it stay in continuous mode? * Add one-shot mode config flag to SEN5X * Add nan checks on sensor data from SEN5X * Working implementation on VOCState * Adds initial timer for SEN55 to not sleep if VOCstate is not stable (1h) * Adds conditions for stability and sensor state * Fixes on VOC state and mode swtiching * Adds a new RHT/Gas only mode, with 3600s stabilization time * Fixes the VOCState buffer mismatch * Fixes SEN50/54/55 model mistake * Adapt SEN5X to new sensor list structure. Improve reclock. * Improve reClockI2C conditions for different variants * Add sleep, wakeUp, pendingForReady, hasSleep functions to PM sensors to save battery * Add SEN5X * Fix merge errors * Small reordering in PMS class for consistency * If one sensor fails, AQ telemetry still reports data * Small formatting fix * Add SEN5X to AQI in ScanI2C * SCD4X now part of AQ module with template list * Fixes difference between idle and sleep * In LowPower, sleep is disabled * Requires testing for I2C clock comms for commands * Remove unnecessary import * Add co2 to serialized AQ metrics * Add SFA30 with new sensor template in AQ module * Update library dependencies in platformio.ini * Fix unitialized variables in SEN5X constructor * Fix missing import * Fix uninitMemberVars * Fix import error for SCD4X * Fix I2CClock logic * Fix not reclocking back to 700000Hz * Fix multiple sensors being read simultaneously * The logic in AQ module is different to the one in EnvironmentTelemetryModule. In Env module, if any sensor fails to getMetrics, no valid flag for the module. This prevents other sensors to report data. * Fix pending clock change in PMSA003 * Cleanup of SEN5X class * Exclude AQ sensor from wio-e5 due to flash limitations * Fix I2C clock change logic * Make sure clock is always set to needed value * Fix returns * Fix trunk * Fix on condition in reclock * Fix trunk * Final SFA30 class implementation * Add HCHO to screen and improve logs * Add metrics to mesh packet serializer * Minor fixes in logs * OCD tidy up of logs * Fix sleep function * Remove old I2C_CLOCK_SPEED code --------- Co-authored-by: nikl <nikl174@mailbox.org> Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de> Co-authored-by: Nashui-Yan <yannashui10@gmail.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Tom Fifield <tom@tomfifield.net> Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> Co-authored-by: WillyJL <me@willyjl.dev> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Log rxBad PacketHeaders with more info (id, relay_node) like printPacket, so we can try to match RX errors to other packets in the logs. (#9614) * Exclude status message module * fix: zero entire public key array instead of only first byte (#9619) * Update protobufs (#9621) * Xiao NRF - define suitable i2c pins for the sub-variants (#8866) Co-authored-by: Christian Walther <cwalther@gmx.ch> * Update src/detect/ScanI2C.cpp Co-authored-by: Wessel <wessel@weebl.me> * Update src/modules/Telemetry/Sensor/SFA30Sensor.cpp Co-authored-by: Wessel <wessel@weebl.me> * Update src/modules/Telemetry/Sensor/SFA30Sensor.cpp Co-authored-by: Wessel <wessel@weebl.me> * Update src/modules/Telemetry/Sensor/SFA30Sensor.cpp Co-authored-by: Wessel <wessel@weebl.me> * Update src/mesh/NodeDB.cpp Co-authored-by: Wessel <wessel@weebl.me> * convert GPS global and some new in gps.cpp to unique_ptr (#9628) Trying this to see if anything bad happen if I were to replace most raw pointers with unique_ptr. I didn't used std::make_unique since it is only supported in C++14 and onwards but until we update esp32 to arduino 3.x the ESP32 xtensa chips use a C++11 std. * replace delete in RedirectablePrint.cpp with std::unique_ptr (#9642) Is part of the unique_ptr modernization effort. * replace delete in EInkDynamicDisplay.{cpp,h} with std::unique_ptr (#9643) Is part of the unique_ptr modernization effort. * Undefine LED_BUILTIN for Heltec v2 variant (#9647) * Undefine LED_BUILTIN for Heltec v2 variant * Undefine LED_BUILTIN for Heltec v2.1 variant --------- Co-authored-by: Jorropo <jorropo.pgm@gmail.com> * replace delete in RadioInterface.cpp with std::unique_ptr (#9645) Is part of the unique_ptr modernization effort. * fix typo in PIN_GPS_SWITCH (#9648) Wasn't caught by CI. * replace delete in CryptoEngine.{cpp,h} with std::unique_ptr (#9649) Is part of the unique_ptr modernization effort. * workaround NCP5623 and LP5562 I2C builds (#9652) Theses two appear to be buggy on r1-neo and nomadstar meteor pro, they rely on Wire.h being included previously to their import. Idk why other platforms using the same smart LEDs are working while theses ones don't. This should make CI green on the dev branch. * replace delete in AudioThread.h with std::unique_ptr (#9651) Is part of the unique_ptr modernization effort. * Add USB_MODE=1 for Station G2 (#9660) * InkHUD: Favorite Map Applet (#9654) * fix a lot of low level cppcheck warnings (#9623) * simplify the observer pattern, since all the called functions are const getters. * use arduino macro over std: for numerical values and refactor local variables in drawScrollbar() * oh, so Cppcheck actually complained about const pointers not being const. * slowly getting out of ifdef hell * fix inkHUD warnings as well * last 2 check warnings * git checks should fail on low defects from now on * Feat/add scd30 (#9609) * Merge develop into SCD30 * Add SCD30 class * Fix logging and admin commands * Minor cleanup and logging improvements * Minor formatting issue Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Improvements on setTemperature * Fix casting float-uint16_t * Pass 100 for resetting temperature offset * Fix issues pointed out by copilot * Add quick reboot to set interval quicker on scd30 * Change saveState to only happen after boot and minor log changes * Fix missing semicolon in one shot mode log --------- Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * fix: respect DontMqttMeBro flag regardless of channel PSK (#9626) The previous PSK check was broken from its introduction in #4643 — memcmp was used in boolean context without comparing to 0, inverting the condition. Since no one noticed for over a year, the PSK-based filtering provided no practical value. Simplifying to always respect the sender's preference is both more correct and easier to reason about. * our firmware action is too clever Update pio_target and add pio_opts for checks. * fix detection of SCD30 by checking if the size of the return from a 2 byte register read is correct (#9664) * fix detection of SCD30 by checking if thee size of the return from a 2 byte register read is correct fix signedness warning in PMSA003 sensor code. * Add alternate path for LPS22HB Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * check EndTransmission for errors and compare returned length to expected value Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * #9623 resolved a local shadow of next_key by converting it to int. (#9665) * zip a few gitrefs down (#9672) * InkHUD: Allow non-system applets to subscribe to input events (#9514) * Allow inkhud user applets to consume inputs with opt-in system Adds a way for applets to subscribe to input events while keeping it off by default to preserve compatibility and expected behaviours. Adds example for use as well. * Add check for nullptr on getActiveApplet uses * Remove redundant includes * Move subscribedInputs to protected * More consistent naming scheme * Fake IAQ values on Non-BSEC2 platforms like Platformio and the original ESP32 (#9663) * BSEC2 Replacement - add approximation for IAQ to non-BSEC2 platforms. - Re-add this sensor to ESP32 targets, and refactor env_extra includes. - Fix C++ 11 compatibility * Check for gas resistance 0 * ULED_BUILTIN for 9m2ibr_aprs_lora_tracker (#9685) --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Jason P <applewiz@mac.com> Co-authored-by: Eric Sesterhenn <eric.sesterhenn@x41-dsec.de> Co-authored-by: Vortetty <33466216+Vortetty@users.noreply.github.com> Co-authored-by: Mattatat25 <108779801+mattatat25@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Max <rekin.m@gmail.com> Co-authored-by: Colby Dillion <colby.dillion@pacshealth.com> Co-authored-by: Austin <vidplace7@gmail.com> Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Chloe Bethel <chloe@9net.org> Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> Co-authored-by: Quency-D <55523105+Quency-D@users.noreply.github.com> Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Co-authored-by: oscgonfer <oscgonfer@users.noreply.github.com> Co-authored-by: nikl <nikl174@mailbox.org> Co-authored-by: Hannes Fuchs <hannes.fuchs+git@0xef.de> Co-authored-by: Nashui-Yan <yannashui10@gmail.com> Co-authored-by: Tom Fifield <tom@tomfifield.net> Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu> Co-authored-by: WillyJL <me@willyjl.dev> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Wessel <wessel@weebl.me> Co-authored-by: Christian Walther <cwalther@gmx.ch> Co-authored-by: Jorropo <jorropo.pgm@gmail.com> Co-authored-by: Eric Barch <ericb@ericbarch.com> Co-authored-by: Clive Blackledge <clive@ansible.org>
1012 lines
33 KiB
C++
1012 lines
33 KiB
C++
#include "graphics/niche/InkHUD/Tile.h"
|
|
#include <cstdint>
|
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
|
|
|
#include "./Applet.h"
|
|
|
|
#include "main.h"
|
|
|
|
#include "RTC.h"
|
|
|
|
using namespace NicheGraphics;
|
|
|
|
InkHUD::AppletFont InkHUD::Applet::fontLarge; // General purpose fonts. Set in nicheGraphics.h
|
|
InkHUD::AppletFont InkHUD::Applet::fontMedium;
|
|
InkHUD::AppletFont InkHUD::Applet::fontSmall;
|
|
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
|
|
|
|
inkhud = InkHUD::getInstance();
|
|
settings = &inkhud->persistence->settings;
|
|
latestMessage = &inkhud->persistence->latestMessage;
|
|
}
|
|
|
|
// Draw a single pixel
|
|
// The raw pixel output generated by AdafruitGFX drawing all passes through here
|
|
// Hand off to the applet's tile, which will in-turn pass to the renderer
|
|
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, static_cast<Color>(color));
|
|
}
|
|
|
|
// Link our applet to a tile
|
|
// This can only be called by Tile::assignApplet
|
|
// The tile determines the applets dimensions
|
|
// Pixel output is passed to tile during render()
|
|
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;
|
|
}
|
|
|
|
// The tile to which our applet is assigned
|
|
InkHUD::Tile *InkHUD::Applet::getTile()
|
|
{
|
|
return assignedTile;
|
|
}
|
|
|
|
// Draw the applet
|
|
void InkHUD::Applet::render(bool full)
|
|
{
|
|
assert(assignedTile); // Ensure that we have a tile
|
|
assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile
|
|
|
|
// WindowManager::update has now consumed the info about our update request
|
|
// Clear everything for future requests
|
|
wantRender = false; // Flag set by requestUpdate
|
|
wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored.
|
|
wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted.
|
|
wantFullRender = true; // Default to a full render
|
|
|
|
updateDimensions();
|
|
resetDrawingSpace();
|
|
onRender(full); // Draw the applet
|
|
|
|
// Handle "Tile Highlighting"
|
|
// Some devices may use an auxiliary button to switch between tiles
|
|
// When this happens, we temporarily highlight the newly focused tile with a border
|
|
|
|
// 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
|
|
// Used by WindowManager::update
|
|
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
|
|
// Used by WindowManager::update
|
|
bool InkHUD::Applet::wantsToAutoshow()
|
|
{
|
|
return wantAutoshow;
|
|
}
|
|
|
|
// Which technique would this applet prefer that the display use to change the image?
|
|
// Used by WindowManager::update
|
|
Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType()
|
|
{
|
|
return wantUpdateType;
|
|
}
|
|
|
|
bool InkHUD::Applet::wantsFullRender()
|
|
{
|
|
return wantFullRender;
|
|
}
|
|
|
|
// Get size of the applet's drawing space from its tile
|
|
// Performed immediately before derived applet's drawing code runs
|
|
void InkHUD::Applet::updateDimensions()
|
|
{
|
|
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(fontSmall);
|
|
}
|
|
|
|
// Sets one or more inputs to enabled/disabled for this applet and if they should be sent to it
|
|
void InkHUD::Applet::setInputsSubscribed(uint8_t input, bool captured)
|
|
{
|
|
if (captured)
|
|
subscribedInputs |= input;
|
|
else
|
|
subscribedInputs &= ~input;
|
|
}
|
|
|
|
// Checks if a specific input is enabled for this applet and should be sent to it
|
|
bool InkHUD::Applet::isInputSubscribed(InputMask input)
|
|
{
|
|
return (subscribedInputs & input) == input;
|
|
}
|
|
|
|
// Tell InkHUD::Renderer 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 renderer has given other applets a chance to process whatever event we just detected,
|
|
// it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground)
|
|
// We should requestUpdate even if our applet is currently background, because this might be changed by autoshow
|
|
void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type, bool full)
|
|
{
|
|
wantRender = true;
|
|
wantUpdateType = type;
|
|
wantFullRender = full;
|
|
inkhud->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 unexpectedly renderer at any time by other InkHUD components
|
|
// 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 stops 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 Renderer 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 (exception: some system applets)
|
|
void InkHUD::Applet::sendToBackground()
|
|
{
|
|
if (foreground) {
|
|
foreground = false;
|
|
onBackground(); // Run derived applet class' handler
|
|
}
|
|
}
|
|
|
|
// Is the applet currently displayed on a tile
|
|
// Note: in some uncommon situations, an applet may be "foreground", and still not visible.
|
|
// This can occur when a system applet is covering the screen (e.g. during BLE pairing)
|
|
// This is not our applets responsibility to handle,
|
|
// as in those situations, the system applet will have "locked" rendering
|
|
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)
|
|
{
|
|
// 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);
|
|
|
|
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, rather than sizing to text (getTextBounds)
|
|
|
|
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);
|
|
}
|
|
|
|
// Print text, specifying the position of any edge / corner of the textbox
|
|
void InkHUD::Applet::printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha, VerticalAlignment va)
|
|
{
|
|
printAt(x, y, text.c_str(), ha, va);
|
|
}
|
|
|
|
// Set which font should be used for subsequent drawing
|
|
// This is AppletFont type, which is a wrapper for AdafruitGFX 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 AdafruitGFX font, with some precalculated dimension data
|
|
InkHUD::AppletFont InkHUD::Applet::getFont()
|
|
{
|
|
return currentFont;
|
|
}
|
|
|
|
// Parse any text which might have "special characters"
|
|
// Re-encodes UTF-8 characters to match our 8-bit encoded fonts
|
|
std::string InkHUD::Applet::parse(const std::string &text)
|
|
{
|
|
return getFont().decodeUTF8(text);
|
|
}
|
|
|
|
// Get the best version of a node's short name available to us
|
|
// Parses any non-ascii chars
|
|
// Swaps for last-four of node-id if the real short name is unknown or can't be rendered (emoji)
|
|
std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node)
|
|
{
|
|
assert(node);
|
|
|
|
// Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.)
|
|
if (node->has_user) {
|
|
std::string parsed = parse(node->user.short_name);
|
|
if (isPrintable(parsed))
|
|
return parsed;
|
|
}
|
|
|
|
// Otherwise, use the "last 4" of node id
|
|
// - if short name unknown, or
|
|
// - if short name is emoji (we can't render this)
|
|
std::string nodeID = hexifyNodeNum(node->num);
|
|
return nodeID.substr(nodeID.length() - 4);
|
|
}
|
|
|
|
// Determine if all characters of a string are printable using the current font
|
|
bool InkHUD::Applet::isPrintable(const std::string &text)
|
|
{
|
|
// Scan for SUB (0x1A), which is the value assigned by AppletFont::applyEncoding if a unicode character is not handled
|
|
for (const char &c : text) {
|
|
if (c == '\x1A')
|
|
return false;
|
|
}
|
|
|
|
// No unprintable characters found
|
|
return true;
|
|
}
|
|
|
|
// 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
|
|
// Wrapper for getTextBounds
|
|
uint16_t InkHUD::Applet::getTextWidth(const std::string &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::Applet::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);
|
|
}
|
|
|
|
// Print text, with word wrapping
|
|
// Avoids splitting words in half, instead moving the entire word to a new line wherever possible
|
|
void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &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 bx, by;
|
|
uint16_t bw, bh;
|
|
for (uint16_t c = 0; c < word.length(); c++) {
|
|
// Shove next char into a c string
|
|
cstr[0] = word[c];
|
|
getTextBounds(cstr, getCursorX(), getCursorY(), &bx, &by, &bw, &bh);
|
|
|
|
// Manual newline, if next character will spill beyond screen edge
|
|
if ((bx + bw) > 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, const 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 = 1738368000 - (SEC_PER_DAY * 30 * 6); // 6 Months prior to Feb 1, 2025 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)
|
|
return "";
|
|
|
|
// Times are invalid: argument time is significantly ahead of RTC
|
|
// Don't give any human readable string
|
|
if (daysAgo < -2)
|
|
return "";
|
|
|
|
// Times are probably invalid: more than 6 months ago
|
|
if (daysAgo > 6 * 30)
|
|
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, either 12 hour or 24 hour
|
|
char clockStr[11];
|
|
if (config.display.use_12h_clock)
|
|
sprintf(clockStr, "%u:%02u %s", (hour % 12 == 0 ? 12 : hour % 12), min, hour > 11 ? "PM" : "AM");
|
|
else
|
|
sprintf(clockStr, "%02u:%02u", hour, min);
|
|
|
|
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++) {
|
|
const 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;
|
|
|
|
// Imperial
|
|
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;
|
|
}
|
|
|
|
// Print text with a "faux bold" effect, by drawing it multiple times, offsetting slightly
|
|
void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, const 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(NicheGraphics::InkHUD::Notification &n)
|
|
{
|
|
// By default, no objection
|
|
return true;
|
|
}
|
|
|
|
// Draw the standard header, used by most Applets
|
|
/*
|
|
┌───────────────────────────────┐
|
|
│ Applet::name here │
|
|
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
└───────────────────────────────┘
|
|
*/
|
|
void InkHUD::Applet::drawHeader(const std::string &text)
|
|
{
|
|
// 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%
|
|
}
|
|
|
|
// Dither near battery
|
|
if (settings->optionalFeatures.batteryIcon) {
|
|
constexpr uint16_t ditherSizePx = 4;
|
|
Tile *batteryTile = ((Applet *)inkhud->getSystemApplet("BatteryIcon"))->getTile();
|
|
const uint16_t batteryTileLeft = batteryTile->getLeft();
|
|
const uint16_t batteryTileTop = batteryTile->getTop();
|
|
const uint16_t batteryTileHeight = batteryTile->getHeight();
|
|
hatchRegion(batteryTileLeft - ditherSizePx, batteryTileTop, ditherSizePx, batteryTileHeight, 2, WHITE);
|
|
}
|
|
}
|
|
|
|
// 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, Color color)
|
|
{
|
|
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)
|
|
/*
|
|
+-----------------------------+
|
|
--| a2 b2/c1 |
|
|
| |
|
|
| |
|
|
| |
|
|
--| a1 b1 c2 |
|
|
+-----------------------------+
|
|
| | | |
|
|
*/
|
|
|
|
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 angle of the path(s)
|
|
// Used to thicken the single pixel paths
|
|
/*
|
|
+-------------------------------+
|
|
| a2 |
|
|
| -| |
|
|
| -/ | |
|
|
| -/ | |
|
|
| -/# | |
|
|
| -/ # | |
|
|
| / # | |
|
|
| a1---------- |
|
|
+-------------------------------+
|
|
*/
|
|
|
|
Distance deltaA = {abs(a2.x - a1.x), abs(a2.y - a1.y)};
|
|
float angle = tanh((float)deltaA.y / deltaA.x);
|
|
|
|
// Distance (at right angle to 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
|
|
/*
|
|
| a2
|
|
| .
|
|
| ..
|
|
| aq1 ..
|
|
| # ..
|
|
| | # ..
|
|
|fromPath.y | # ..
|
|
| +----a1
|
|
|
|
|
| fromPath.x
|
|
+--------------------------------
|
|
*/
|
|
|
|
Distance fromPath;
|
|
fromPath.x = cos(radians(90) - angle) * logoTh * 0.5;
|
|
fromPath.y = sin(radians(90) - angle) * logoTh * 0.5;
|
|
|
|
// Make the paths thick
|
|
// Corner points for the rectangles (quads):
|
|
/*
|
|
|
|
aq2
|
|
a2
|
|
/ aq3
|
|
/
|
|
/
|
|
aq1 /
|
|
a1
|
|
aq3
|
|
*/
|
|
|
|
// Filled as two triangles per quad:
|
|
/*
|
|
aq2 #
|
|
# ###
|
|
## # aq3
|
|
## ### -
|
|
## #### -/
|
|
## ### -/
|
|
## #### -/
|
|
aq1 ## -/
|
|
--- -/
|
|
\---aq4
|
|
*/
|
|
|
|
// 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, color);
|
|
fillTriangle(aq1.x, aq1.y, aq3.x, aq3.y, aq4.x, aq4.y, color);
|
|
|
|
// 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, color);
|
|
fillTriangle(bq1.x, bq1.y, bq3.x, bq3.y, bq4.x, bq4.y, color);
|
|
|
|
// Make the path thick: 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, color);
|
|
fillTriangle(cq1.x, cq1.y, cq3.x, cq3.y, cq4.x, cq4.y, color);
|
|
|
|
// Radius the intersection of quad b and quad c
|
|
/*
|
|
b2 / c1
|
|
####
|
|
## ##
|
|
/ \
|
|
/ \/ \
|
|
/ /\ \
|
|
/ / \ \
|
|
|
|
*/
|
|
|
|
// 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 re-deriving it
|
|
int16_t capRad = sqrt(pow(fromPath.x, 2) + pow(fromPath.y, 2));
|
|
fillCircle(b2.x, b2.y, capRad, color);
|
|
}
|
|
}
|
|
|
|
#endif |