diff --git a/.envrc b/.envrc
new file mode 100644
index 000000000..65326bb6d
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use nix
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 769603202..d6d97c6c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,3 +50,6 @@ idf_component.yml
CMakeLists.txt
/sdkconfig.*
.dummy/*
+
+# PYTHONPATH used by the Nix shell
+.python3
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 000000000..e7a4c7ff7
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,44 @@
+{
+ "nodes": {
+ "flake-compat": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1767039857,
+ "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
+ "owner": "NixOS",
+ "repo": "flake-compat",
+ "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "repo": "flake-compat",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1766314097,
+ "narHash": "sha256-laJftWbghBehazn/zxVJ8NdENVgjccsWAdAqKXhErrM=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "306ea70f9eb0fb4e040f8540e2deab32ed7e2055",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-compat": "flake-compat",
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 000000000..1af493c6d
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,66 @@
+{
+ description = "Nix flake to compile Meshtastic firmware";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+
+ # Shim to make flake.nix work with stable Nix.
+ flake-compat = {
+ url = "github:NixOS/flake-compat";
+ flake = false;
+ };
+ };
+
+ outputs =
+ inputs:
+ let
+ lib = inputs.nixpkgs.lib;
+
+ forAllSystems =
+ fn:
+ lib.genAttrs lib.systems.flakeExposed (
+ system:
+ fn {
+ pkgs = import inputs.nixpkgs {
+ inherit system;
+ };
+ inherit system;
+ }
+ );
+ in
+ {
+ devShells = forAllSystems (
+ { pkgs, ... }:
+ let
+ python3 = pkgs.python312.withPackages (
+ ps: with ps; [
+ google
+ ]
+ );
+ in
+ {
+ default = pkgs.mkShell {
+ buildInputs = with pkgs; [
+ python3
+ platformio
+ ];
+
+ shellHook = ''
+ # Set up PlatformIO to use a local core directory.
+ export PLATFORMIO_CORE_DIR=$PWD/.platformio
+ # Tell pip to put packages into $PIP_PREFIX instead of the usual
+ # location. This is especially necessary under NixOS to avoid having
+ # pip trying to write to the read-only Nix store. For more info,
+ # see https://wiki.nixos.org/wiki/Python
+ export PIP_PREFIX=$PWD/.python3
+ export PYTHONPATH="$PIP_PREFIX/${python3.sitePackages}"
+ export PATH="$PIP_PREFIX/bin:$PATH"
+ # Avoids reproducibility issues with some Python packages
+ # See https://nixos.org/manual/nixpkgs/stable/#python-setup.py-bdist_wheel-cannot-create-.whl
+ unset SOURCE_DATE_EPOCH
+ '';
+ };
+ }
+ );
+ };
+}
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 000000000..692cd4df8
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,12 @@
+(import (
+ let
+ lock = builtins.fromJSON (builtins.readFile ./flake.lock);
+ nodeName = lock.nodes.root.inputs.flake-compat;
+ in
+ fetchTarball {
+ url =
+ lock.nodes.${nodeName}.locked.url
+ or "https://github.com/NixOS/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz";
+ sha256 = lock.nodes.${nodeName}.locked.narHash;
+ }
+) { src = ./.; }).shellNix
diff --git a/src/Power.cpp b/src/Power.cpp
index b2a4ddaaf..b211d760e 100644
--- a/src/Power.cpp
+++ b/src/Power.cpp
@@ -816,6 +816,9 @@ void Power::shutdown()
#endif
#ifdef PIN_LED3
ledOff(PIN_LED3);
+#endif
+#ifdef LED_NOTIFICATION
+ ledOff(LED_NOTIFICATION);
#endif
doDeepSleep(DELAY_FOREVER, true, true);
#elif defined(ARCH_PORTDUINO)
diff --git a/src/configuration.h b/src/configuration.h
index f7b438272..66fa4492d 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -390,9 +390,6 @@ along with this program. If not, see .
#ifndef HAS_RADIO
#define HAS_RADIO 0
#endif
-#ifndef HAS_RTC
-#define HAS_RTC 0
-#endif
#ifndef HAS_CPU_SHUTDOWN
#define HAS_CPU_SHUTDOWN 0
#endif
@@ -428,12 +425,16 @@ along with this program. If not, see .
#define HAS_RGB_LED
#endif
-#ifndef LED_STATE_OFF
-#define LED_STATE_OFF 0
-#endif
#ifndef LED_STATE_ON
#define LED_STATE_ON 1
#endif
+#ifndef LED_STATE_OFF
+#define LED_STATE_OFF (LED_STATE_ON ^ 1)
+#endif
+
+#ifndef ledOff
+#define ledOff(pin) pinMode(pin, INPUT)
+#endif
// default mapping of pins
#if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN)
diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp
index b13406e96..3bca6f6ec 100644
--- a/src/gps/RTC.cpp
+++ b/src/gps/RTC.cpp
@@ -276,10 +276,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
settimeofday(tv, NULL);
#endif
-#if HAS_RTC
readFromRTC();
-#endif
-
return RTCSetResultSuccess;
} else {
return RTCSetResultNotSet; // RTC was already set with a higher quality time
diff --git a/src/graphics/niche/InkHUD/Applet.cpp b/src/graphics/niche/InkHUD/Applet.cpp
index 1e89ebe1b..ccdd76f97 100644
--- a/src/graphics/niche/InkHUD/Applet.cpp
+++ b/src/graphics/niche/InkHUD/Applet.cpp
@@ -55,7 +55,7 @@ InkHUD::Tile *InkHUD::Applet::getTile()
}
// Draw the applet
-void InkHUD::Applet::render()
+void InkHUD::Applet::render(bool full)
{
assert(assignedTile); // Ensure that we have a tile
assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile
@@ -65,10 +65,11 @@ void InkHUD::Applet::render()
wantRender = false; // Flag set by requestUpdate
wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored.
wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted.
+ wantFullRender = true; // Default to a full render
updateDimensions();
resetDrawingSpace();
- onRender(); // Derived applet's drawing takes place here
+ onRender(full); // Draw the applet
// Handle "Tile Highlighting"
// Some devices may use an auxiliary button to switch between tiles
@@ -115,6 +116,11 @@ Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType()
return wantUpdateType;
}
+bool InkHUD::Applet::wantsFullRender()
+{
+ return wantFullRender;
+}
+
// Get size of the applet's drawing space from its tile
// Performed immediately before derived applet's drawing code runs
void InkHUD::Applet::updateDimensions()
@@ -142,10 +148,11 @@ void InkHUD::Applet::resetDrawingSpace()
// Once the renderer has given other applets a chance to process whatever event we just detected,
// it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground)
// We should requestUpdate even if our applet is currently background, because this might be changed by autoshow
-void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type)
+void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type, bool full)
{
wantRender = true;
wantUpdateType = type;
+ wantFullRender = full;
inkhud->requestUpdate();
}
diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h
index b35ca5cc0..69d35a234 100644
--- a/src/graphics/niche/InkHUD/Applet.h
+++ b/src/graphics/niche/InkHUD/Applet.h
@@ -64,10 +64,11 @@ class Applet : public GFX
// Rendering
- void render(); // Draw the applet
+ void render(bool full); // Draw the applet
bool wantsToRender(); // Check whether applet wants to render
bool wantsToAutoshow(); // Check whether applet wants to become foreground
Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer
+ bool wantsFullRender(); // Check whether applet wants to render over its previous render
void updateDimensions(); // Get current size from tile
void resetDrawingSpace(); // Makes sure every render starts with same parameters
@@ -82,7 +83,7 @@ class Applet : public GFX
// Event handlers
- virtual void onRender() = 0; // All drawing happens here
+ virtual void onRender(bool full) = 0; // For drawing the applet
virtual void onActivate() {}
virtual void onDeactivate() {}
virtual void onForeground() {}
@@ -96,6 +97,9 @@ class Applet : public GFX
virtual void onNavDown() {}
virtual void onNavLeft() {}
virtual void onNavRight() {}
+ virtual void onFreeText(char c) {}
+ virtual void onFreeTextDone() {}
+ virtual void onFreeTextCancel() {}
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
@@ -108,8 +112,9 @@ class Applet : public GFX
protected:
void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here
- void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update
- void requestAutoshow(); // Ask for applet to be moved to foreground
+ void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED,
+ bool full = true); // Ask WindowManager to schedule a display update
+ void requestAutoshow(); // Ask for applet to be moved to foreground
uint16_t X(float f); // Map applet width, mapped from 0 to 1.0
uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0
@@ -164,6 +169,7 @@ class Applet : public GFX
bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground?
NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType =
NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display
+ bool wantFullRender = true; // Render with a fresh canvas
using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly
using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager.
diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp
index d383a11e4..4cf83966b 100644
--- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp
+++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp
@@ -4,7 +4,7 @@
using namespace NicheGraphics;
-void InkHUD::MapApplet::onRender()
+void InkHUD::MapApplet::onRender(bool full)
{
// Abort if no markers to render
if (!enoughMarkers()) {
diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h
index f45a36071..11dfb39d9 100644
--- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h
+++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h
@@ -27,7 +27,7 @@ namespace NicheGraphics::InkHUD
class MapApplet : public Applet
{
public:
- void onRender() override;
+ void onRender(bool full) override;
protected:
virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes
diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp
index 5c9906fba..9794c3efb 100644
--- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp
+++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp
@@ -103,7 +103,7 @@ uint8_t InkHUD::NodeListApplet::maxCards()
}
// Draw, using info which derived applet placed into NodeListApplet::cards for us
-void InkHUD::NodeListApplet::onRender()
+void InkHUD::NodeListApplet::onRender(bool full)
{
// ================================
diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h
index c2340027b..8babdba03 100644
--- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h
+++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h
@@ -46,7 +46,7 @@ class NodeListApplet : public Applet, public MeshModule
public:
NodeListApplet(const char *name);
- void onRender() override;
+ void onRender(bool full) override;
bool wantPacket(const meshtastic_MeshPacket *p) override;
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
diff --git a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp
index c52719e55..71b6d9a7a 100644
--- a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp
+++ b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp
@@ -6,7 +6,7 @@ using namespace NicheGraphics;
// All drawing happens here
// Our basic example doesn't do anything useful. It just passively prints some text.
-void InkHUD::BasicExampleApplet::onRender()
+void InkHUD::BasicExampleApplet::onRender(bool full)
{
printAt(0, 0, "Hello, World!");
diff --git a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h
index aed63cdc8..a36f6e8d5 100644
--- a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h
+++ b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h
@@ -28,7 +28,7 @@ class BasicExampleApplet : public Applet
// You must have an onRender() method
// All drawing happens here
- void onRender() override;
+ void onRender(bool full) override;
};
} // namespace NicheGraphics::InkHUD
diff --git a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp
index 6b02f4c92..cf3fd7714 100644
--- a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp
+++ b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp
@@ -35,7 +35,7 @@ ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_Mesh
// We can trigger a render by calling requestUpdate()
// Render might be called by some external source
// We should always be ready to draw
-void InkHUD::NewMsgExampleApplet::onRender()
+void InkHUD::NewMsgExampleApplet::onRender(bool full)
{
printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0)
diff --git a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h
index 22670a0f0..599f08a7a 100644
--- a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h
+++ b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h
@@ -34,7 +34,7 @@ class NewMsgExampleApplet : public Applet, public SinglePortModule
NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {}
// All drawing happens here
- void onRender() override;
+ void onRender(bool full) override;
// Your applet might also want to use some of these
// Useful for setting up or tidying up
diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp
index 67ef87f41..3afa80149 100644
--- a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp
+++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp
@@ -10,7 +10,7 @@ InkHUD::AlignStickApplet::AlignStickApplet()
bringToForeground();
}
-void InkHUD::AlignStickApplet::onRender()
+void InkHUD::AlignStickApplet::onRender(bool full)
{
setFont(fontMedium);
printAt(0, 0, "Align Joystick:");
@@ -152,19 +152,17 @@ void InkHUD::AlignStickApplet::onBackground()
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
- inkhud->forceUpdate(EInk::UpdateTypes::FULL);
+ inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
}
void InkHUD::AlignStickApplet::onButtonLongPress()
{
sendToBackground();
- inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onExitLong()
{
sendToBackground();
- inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onNavUp()
@@ -172,7 +170,6 @@ void InkHUD::AlignStickApplet::onNavUp()
settings->joystick.aligned = true;
sendToBackground();
- inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onNavDown()
@@ -181,7 +178,6 @@ void InkHUD::AlignStickApplet::onNavDown()
settings->joystick.aligned = true;
sendToBackground();
- inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onNavLeft()
@@ -190,7 +186,6 @@ void InkHUD::AlignStickApplet::onNavLeft()
settings->joystick.aligned = true;
sendToBackground();
- inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
void InkHUD::AlignStickApplet::onNavRight()
@@ -199,7 +194,6 @@ void InkHUD::AlignStickApplet::onNavRight()
settings->joystick.aligned = true;
sendToBackground();
- inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
#endif
\ No newline at end of file
diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h
index 8dba33165..7c8d00155 100644
--- a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h
+++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h
@@ -23,7 +23,7 @@ class AlignStickApplet : public SystemApplet
public:
AlignStickApplet();
- void onRender() override;
+ void onRender(bool full) override;
void onForeground() override;
void onBackground() override;
void onButtonLongPress() override;
diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp
index 4f99d99ee..0cc6f50ed 100644
--- a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp
+++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp
@@ -6,6 +6,8 @@ using namespace NicheGraphics;
InkHUD::BatteryIconApplet::BatteryIconApplet()
{
+ alwaysRender = true; // render everytime the screen is updated
+
// Show at boot, if user has previously enabled the feature
if (settings->optionalFeatures.batteryIcon)
bringToForeground();
@@ -44,7 +46,7 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
return 0; // Tell Observable to continue informing other observers
}
-void InkHUD::BatteryIconApplet::onRender()
+void InkHUD::BatteryIconApplet::onRender(bool full)
{
// Fill entire tile
// - size of icon controlled by size of tile
diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h
index e5b4172be..ceaf88d7f 100644
--- a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h
+++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h
@@ -23,7 +23,7 @@ class BatteryIconApplet : public SystemApplet
public:
BatteryIconApplet();
- void onRender() override;
+ void onRender(bool full) override;
int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available
private:
diff --git a/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.cpp
new file mode 100644
index 000000000..57581d56b
--- /dev/null
+++ b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.cpp
@@ -0,0 +1,257 @@
+#ifdef MESHTASTIC_INCLUDE_INKHUD
+#include "./KeyboardApplet.h"
+
+using namespace NicheGraphics;
+
+InkHUD::KeyboardApplet::KeyboardApplet()
+{
+ // Calculate row widths
+ for (uint8_t row = 0; row < KBD_ROWS; row++) {
+ rowWidths[row] = 0;
+ for (uint8_t col = 0; col < KBD_COLS; col++)
+ rowWidths[row] += keyWidths[row * KBD_COLS + col];
+ }
+}
+
+void InkHUD::KeyboardApplet::onRender(bool full)
+{
+ uint16_t em = fontSmall.lineHeight(); // 16 pt
+ uint16_t keyH = Y(1.0) / KBD_ROWS;
+ int16_t keyTopPadding = (keyH - fontSmall.lineHeight()) / 2;
+
+ if (full) { // Draw full keyboard
+ for (uint8_t row = 0; row < KBD_ROWS; row++) {
+
+ // Calculate the remaining space to be used as padding
+ int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
+
+ // Draw keys
+ uint16_t xPos = 0;
+ for (uint8_t col = 0; col < KBD_COLS; col++) {
+ Color fgcolor = BLACK;
+ uint8_t index = row * KBD_COLS + col;
+ uint16_t keyX = ((xPos * em) >> 4) + ((col * keyXPadding) / (KBD_COLS - 1));
+ uint16_t keyY = row * keyH;
+ uint16_t keyW = (keyWidths[index] * em) >> 4;
+ if (index == selectedKey) {
+ fgcolor = WHITE;
+ fillRect(keyX, keyY, keyW, keyH, BLACK);
+ }
+ drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[index], fgcolor);
+ xPos += keyWidths[index];
+ }
+ }
+ } else { // Only draw the difference
+ if (selectedKey != prevSelectedKey) {
+ // Draw previously selected key
+ uint8_t row = prevSelectedKey / KBD_COLS;
+ int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
+ uint16_t xPos = 0;
+ for (uint8_t i = prevSelectedKey - (prevSelectedKey % KBD_COLS); i < prevSelectedKey; i++)
+ xPos += keyWidths[i];
+ uint16_t keyX = ((xPos * em) >> 4) + (((prevSelectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1));
+ uint16_t keyY = row * keyH;
+ uint16_t keyW = (keyWidths[prevSelectedKey] * em) >> 4;
+ fillRect(keyX, keyY, keyW, keyH, WHITE);
+ drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[prevSelectedKey], BLACK);
+
+ // Draw newly selected key
+ row = selectedKey / KBD_COLS;
+ keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
+ xPos = 0;
+ for (uint8_t i = selectedKey - (selectedKey % KBD_COLS); i < selectedKey; i++)
+ xPos += keyWidths[i];
+ keyX = ((xPos * em) >> 4) + (((selectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1));
+ keyY = row * keyH;
+ keyW = (keyWidths[selectedKey] * em) >> 4;
+ fillRect(keyX, keyY, keyW, keyH, BLACK);
+ drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[selectedKey], WHITE);
+ }
+ }
+
+ prevSelectedKey = selectedKey;
+}
+
+// Draw the key label corresponding to the char
+// for most keys it draws the character itself
+// for ['\b', '\n', ' ', '\x1b'] it draws special glyphs
+void InkHUD::KeyboardApplet::drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color)
+{
+ if (key == '\b') {
+ // Draw backspace glyph: 13 x 9 px
+ /**
+ * [][][][][][][][][]
+ * [][] []
+ * [][] [] [] []
+ * [][] [] [] []
+ * [][] [] []
+ * [][] [] [] []
+ * [][] [] [] []
+ * [][] []
+ * [][][][][][][][][]
+ */
+ const uint8_t bsBitmap[] = {0x0f, 0xf8, 0x18, 0x08, 0x32, 0x28, 0x61, 0x48, 0xc0,
+ 0x88, 0x61, 0x48, 0x32, 0x28, 0x18, 0x08, 0x0f, 0xf8};
+ uint16_t leftPadding = (width - 13) >> 1;
+ drawBitmap(left + leftPadding, top + 1, bsBitmap, 13, 9, color);
+ } else if (key == '\n') {
+ // Draw done glyph: 12 x 9 px
+ /**
+ * [][]
+ * [][]
+ * [][]
+ * [][]
+ * [][]
+ * [][] [][]
+ * [][] [][]
+ * [][][]
+ * []
+ */
+ const uint8_t doneBitmap[] = {0x00, 0x30, 0x00, 0x60, 0x00, 0xc0, 0x01, 0x80, 0x03,
+ 0x00, 0xc6, 0x00, 0x6c, 0x00, 0x38, 0x00, 0x10, 0x00};
+ uint16_t leftPadding = (width - 12) >> 1;
+ drawBitmap(left + leftPadding, top + 1, doneBitmap, 12, 9, color);
+ } else if (key == ' ') {
+ // Draw space glyph: 13 x 9 px
+ /**
+ *
+ *
+ *
+ *
+ * [] []
+ * [] []
+ * [][][][][][][][][][][][][]
+ *
+ *
+ */
+ const uint8_t spaceBitmap[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
+ 0x08, 0x80, 0x08, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00};
+ uint16_t leftPadding = (width - 13) >> 1;
+ drawBitmap(left + leftPadding, top + 1, spaceBitmap, 13, 9, color);
+ } else if (key == '\x1b') {
+ setTextColor(color);
+ std::string keyText = "ESC";
+ uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1;
+ printAt(left + leftPadding, top, keyText);
+ } else {
+ setTextColor(color);
+ if (key >= 0x61)
+ key -= 32; // capitalize
+ std::string keyText = std::string(1, key);
+ uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1;
+ printAt(left + leftPadding, top, keyText);
+ }
+}
+
+void InkHUD::KeyboardApplet::onForeground()
+{
+ handleInput = true; // Intercept the button input for our applet
+
+ // Select the first key
+ selectedKey = 0;
+ prevSelectedKey = 0;
+}
+
+void InkHUD::KeyboardApplet::onBackground()
+{
+ handleInput = false;
+}
+
+void InkHUD::KeyboardApplet::onButtonShortPress()
+{
+ char key = keys[selectedKey];
+ if (key == '\n') {
+ inkhud->freeTextDone();
+ inkhud->closeKeyboard();
+ } else if (key == '\x1b') {
+ inkhud->freeTextCancel();
+ inkhud->closeKeyboard();
+ } else {
+ inkhud->freeText(key);
+ }
+}
+
+void InkHUD::KeyboardApplet::onButtonLongPress()
+{
+ char key = keys[selectedKey];
+ if (key == '\n') {
+ inkhud->freeTextDone();
+ inkhud->closeKeyboard();
+ } else if (key == '\x1b') {
+ inkhud->freeTextCancel();
+ inkhud->closeKeyboard();
+ } else {
+ if (key >= 0x61)
+ key -= 32; // capitalize
+ inkhud->freeText(key);
+ }
+}
+
+void InkHUD::KeyboardApplet::onExitShort()
+{
+ inkhud->freeTextCancel();
+ inkhud->closeKeyboard();
+}
+
+void InkHUD::KeyboardApplet::onExitLong()
+{
+ inkhud->freeTextCancel();
+ inkhud->closeKeyboard();
+}
+
+void InkHUD::KeyboardApplet::onNavUp()
+{
+ if (selectedKey < KBD_COLS) // wrap
+ selectedKey += KBD_COLS * (KBD_ROWS - 1);
+ else // move 1 row back
+ selectedKey -= KBD_COLS;
+
+ // Request rendering over the previously drawn render
+ requestUpdate(EInk::UpdateTypes::FAST, false);
+ // Force an update to bypass lockRequests
+ inkhud->forceUpdate(EInk::UpdateTypes::FAST);
+}
+
+void InkHUD::KeyboardApplet::onNavDown()
+{
+ selectedKey += KBD_COLS;
+ selectedKey %= (KBD_COLS * KBD_ROWS);
+
+ // Request rendering over the previously drawn render
+ requestUpdate(EInk::UpdateTypes::FAST, false);
+ // Force an update to bypass lockRequests
+ inkhud->forceUpdate(EInk::UpdateTypes::FAST);
+}
+
+void InkHUD::KeyboardApplet::onNavLeft()
+{
+ if (selectedKey % KBD_COLS == 0) // wrap
+ selectedKey += KBD_COLS - 1;
+ else // move 1 column back
+ selectedKey--;
+
+ // Request rendering over the previously drawn render
+ requestUpdate(EInk::UpdateTypes::FAST, false);
+ // Force an update to bypass lockRequests
+ inkhud->forceUpdate(EInk::UpdateTypes::FAST);
+}
+
+void InkHUD::KeyboardApplet::onNavRight()
+{
+ if (selectedKey % KBD_COLS == KBD_COLS - 1) // wrap
+ selectedKey -= KBD_COLS - 1;
+ else // move 1 column forward
+ selectedKey++;
+
+ // Request rendering over the previously drawn render
+ requestUpdate(EInk::UpdateTypes::FAST, false);
+ // Force an update to bypass lockRequests
+ inkhud->forceUpdate(EInk::UpdateTypes::FAST);
+}
+
+uint16_t InkHUD::KeyboardApplet::getKeyboardHeight()
+{
+ const uint16_t keyH = fontSmall.lineHeight() * 1.2;
+ return keyH * KBD_ROWS;
+}
+#endif
diff --git a/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h
new file mode 100644
index 000000000..306a8d8e3
--- /dev/null
+++ b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h
@@ -0,0 +1,66 @@
+#ifdef MESHTASTIC_INCLUDE_INKHUD
+
+/*
+
+System Applet to render an on-screeen keyboard
+
+*/
+
+#pragma once
+
+#include "configuration.h"
+#include "graphics/niche/InkHUD/InkHUD.h"
+#include "graphics/niche/InkHUD/SystemApplet.h"
+#include
+namespace NicheGraphics::InkHUD
+{
+
+class KeyboardApplet : public SystemApplet
+{
+ public:
+ KeyboardApplet();
+
+ void onRender(bool full) override;
+ void onForeground() override;
+ void onBackground() override;
+ void onButtonShortPress() override;
+ void onButtonLongPress() override;
+ void onExitShort() override;
+ void onExitLong() override;
+ void onNavUp() override;
+ void onNavDown() override;
+ void onNavLeft() override;
+ void onNavRight() override;
+
+ static uint16_t getKeyboardHeight(); // used to set the keyboard tile height
+
+ private:
+ void drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color);
+
+ static const uint8_t KBD_COLS = 11;
+ static const uint8_t KBD_ROWS = 4;
+
+ const char keys[KBD_COLS * KBD_ROWS] = {
+ '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b', // row 0
+ 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n', // row 1
+ 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '!', ' ', // row 2
+ 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '?', '\x1b' // row 3
+ };
+
+ // This array represents the widths of each key in points
+ // 16 pt = line height of the text
+ const uint16_t keyWidths[KBD_COLS * KBD_ROWS] = {
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 0
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 1
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 2
+ 16, 16, 16, 16, 16, 16, 16, 10, 10, 12, 40 // row 3
+ };
+
+ uint16_t rowWidths[KBD_ROWS];
+ uint8_t selectedKey = 0; // selected key index
+ uint8_t prevSelectedKey = 0;
+};
+
+} // namespace NicheGraphics::InkHUD
+
+#endif
diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp
index 4b55529bb..b2c58fc60 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp
+++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp
@@ -30,7 +30,7 @@ InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet")
// This is then drawn with a FULL refresh by Renderer::begin
}
-void InkHUD::LogoApplet::onRender()
+void InkHUD::LogoApplet::onRender(bool full)
{
// Size of the region which the logo should "scale to fit"
uint16_t logoWLimit = X(0.8);
@@ -120,7 +120,7 @@ void InkHUD::LogoApplet::onBackground()
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
- inkhud->forceUpdate(EInk::UpdateTypes::FULL);
+ inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
}
// Begin displaying the screen which is shown at shutdown
@@ -138,10 +138,10 @@ void InkHUD::LogoApplet::onShutdown()
// Intention is to restore display health.
inverted = true;
- inkhud->forceUpdate(Drivers::EInk::FULL, false);
+ inkhud->forceUpdate(Drivers::EInk::FULL, true, false);
delay(1000); // Cooldown. Back to back updates aren't great for health.
inverted = false;
- inkhud->forceUpdate(Drivers::EInk::FULL, false);
+ inkhud->forceUpdate(Drivers::EInk::FULL, true, false);
delay(1000); // Cooldown
// Prepare for the powered-off screen now
@@ -176,7 +176,7 @@ void InkHUD::LogoApplet::onReboot()
textTitle = "Rebooting...";
fontTitle = fontSmall;
- inkhud->forceUpdate(Drivers::EInk::FULL, false);
+ inkhud->forceUpdate(Drivers::EInk::FULL, true, false);
// Perform the update right now, waiting here until complete
}
diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h
index 37f940453..d70dcc7b2 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h
+++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h
@@ -21,7 +21,7 @@ class LogoApplet : public SystemApplet, public concurrency::OSThread
{
public:
LogoApplet();
- void onRender() override;
+ void onRender(bool full) override;
void onForeground() override;
void onBackground() override;
void onShutdown() override;
diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h
index 74ad5c85f..7ec76292b 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h
+++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h
@@ -19,10 +19,10 @@ namespace NicheGraphics::InkHUD
enum MenuAction {
NO_ACTION,
SEND_PING,
+ FREE_TEXT,
STORE_CANNEDMESSAGE_SELECTION,
SEND_CANNEDMESSAGE,
SHUTDOWN,
- BACK,
NEXT_TILE,
TOGGLE_BACKLIGHT,
TOGGLE_GPS,
diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
index 93d2c6b83..6a141f73e 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
+++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
@@ -90,6 +90,8 @@ void InkHUD::MenuApplet::onForeground()
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
OSThread::enabled = true;
+ freeTextMode = false;
+
// Upgrade the refresh to FAST, for guaranteed responsiveness
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
@@ -116,6 +118,8 @@ void InkHUD::MenuApplet::onBackground()
SystemApplet::lockRequests = false;
SystemApplet::handleInput = false;
+ handleFreeText = false;
+
// Restore the user applet whose tile we borrowed
if (borrowedTileOwner)
borrowedTileOwner->bringToForeground();
@@ -325,10 +329,6 @@ void InkHUD::MenuApplet::execute(MenuItem item)
}
break;
- case BACK:
- showPage(item.nextPage);
- return;
-
case NEXT_TILE:
inkhud->nextTile();
// Unselect menu item after tile change
@@ -344,12 +344,26 @@ void InkHUD::MenuApplet::execute(MenuItem item)
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL);
break;
+ case FREE_TEXT:
+ OSThread::enabled = false;
+ handleFreeText = true;
+ cm.freeTextItem.rawText.erase(); // clear the previous freetext message
+ freeTextMode = true; // render input field instead of normal menu
+ // Open the on-screen keyboard if the joystick is enabled
+ if (settings->joystick.enabled)
+ inkhud->openKeyboard();
+ break;
+
case STORE_CANNEDMESSAGE_SELECTION:
- cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
+ if (!settings->joystick.enabled)
+ cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
+ else
+ cm.selectedMessageItem = &cm.messageItems.at(cursor - 2); // Minus two: offset for the "Send Ping" and free text entry
break;
case SEND_CANNEDMESSAGE:
cm.selectedRecipientItem = &cm.recipientItems.at(cursor);
+ // send selected message
sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str());
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here
break;
@@ -868,6 +882,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
switch (page) {
case ROOT:
+ previousPage = MenuPage::EXIT;
// Optional: next applet
if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1)
items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown
@@ -878,7 +893,6 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
items.push_back(MenuItem("Node Config", MenuPage::NODE_CONFIG));
items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
- previousPage = MenuPage::EXIT;
break;
case SEND:
@@ -888,11 +902,12 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
case CANNEDMESSAGE_RECIPIENT:
populateRecipientPage();
- previousPage = MenuPage::OPTIONS;
+ previousPage = MenuPage::SEND;
break;
case OPTIONS:
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::ROOT));
+ previousPage = MenuPage::ROOT;
+ items.push_back(MenuItem("Back", previousPage));
// Optional: backlight
if (settings->optionalMenuItems.backlight)
items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label
@@ -916,31 +931,32 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
invertedColors = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
items.push_back(MenuItem("Invert Color", MenuAction::TOGGLE_INVERT_COLOR, MenuPage::OPTIONS, &invertedColors));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
- previousPage = MenuPage::ROOT;
break;
case APPLETS:
- populateAppletPage(); // must be first
- items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS));
- items.push_back(MenuItem("Exit", MenuPage::EXIT));
previousPage = MenuPage::OPTIONS;
+ populateAppletPage(); // must be first
+ items.insert(items.begin(), MenuItem("Back", previousPage));
+ items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
case AUTOSHOW:
- populateAutoshowPage(); // must be first
- items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS));
- items.push_back(MenuItem("Exit", MenuPage::EXIT));
previousPage = MenuPage::OPTIONS;
+ populateAutoshowPage(); // must be first
+ items.insert(items.begin(), MenuItem("Back", previousPage));
+ items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
case RECENTS:
+ previousPage = MenuPage::OPTIONS;
populateRecentsPage(); // builds only the options
- items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS));
+ items.insert(items.begin(), MenuItem("Back", previousPage));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
case NODE_CONFIG:
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::ROOT));
+ previousPage = MenuPage::ROOT;
+ items.push_back(MenuItem("Back", previousPage));
// Radio Config Section
items.push_back(MenuItem::Header("Radio Config"));
items.push_back(MenuItem("LoRa", MenuPage::NODE_CONFIG_LORA));
@@ -965,8 +981,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
break;
case NODE_CONFIG_DEVICE: {
-
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
+ previousPage = MenuPage::NODE_CONFIG;
+ items.push_back(MenuItem("Back", previousPage));
const char *role = DisplayFormatters::getDeviceRole(config.device.role);
nodeConfigLabels.emplace_back("Role: " + std::string(role));
@@ -981,7 +997,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_POSITION: {
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
+ previousPage = MenuPage::NODE_CONFIG;
+ items.push_back(MenuItem("Back", previousPage));
#if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS
const auto mode = config.position.gps_mode;
if (mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) {
@@ -996,7 +1013,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_POWER: {
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
+ previousPage = MenuPage::NODE_CONFIG;
+ items.push_back(MenuItem("Back", previousPage));
#if defined(ARCH_ESP32)
items.push_back(MenuItem("Powersave", MenuAction::TOGGLE_POWER_SAVE, MenuPage::EXIT, &config.power.is_power_saving));
#endif
@@ -1029,7 +1047,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_POWER_ADC_CAL: {
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_POWER));
+ previousPage = MenuPage::NODE_CONFIG_POWER;
+ items.push_back(MenuItem("Back", previousPage));
// Instruction text (header-style, non-selectable)
items.push_back(MenuItem::Header("Run on full charge Only"));
@@ -1042,7 +1061,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_NETWORK: {
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
+ previousPage = MenuPage::NODE_CONFIG;
+ items.push_back(MenuItem("Back", previousPage));
const char *wifiLabel = config.network.wifi_enabled ? "WiFi: On" : "WiFi: Off";
@@ -1099,7 +1119,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_DISPLAY: {
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
+ previousPage = MenuPage::NODE_CONFIG;
+ items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::NODE_CONFIG_DISPLAY,
&config.display.use_12h_clock));
@@ -1114,7 +1135,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_BLUETOOTH: {
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
+ previousPage = MenuPage::NODE_CONFIG;
+ items.push_back(MenuItem("Back", previousPage));
const char *btLabel = config.bluetooth.enabled ? "Bluetooth: On" : "Bluetooth: Off";
items.push_back(MenuItem(btLabel, MenuAction::TOGGLE_BLUETOOTH, MenuPage::EXIT));
@@ -1127,8 +1149,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_LORA: {
-
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
+ previousPage = MenuPage::NODE_CONFIG;
+ items.push_back(MenuItem("Back", previousPage));
const char *region = myRegion ? myRegion->name : "Unset";
nodeConfigLabels.emplace_back("Region: " + std::string(region));
@@ -1150,7 +1172,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_CHANNELS: {
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
+ previousPage = MenuPage::NODE_CONFIG;
+ items.push_back(MenuItem("Back", previousPage));
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
meshtastic_Channel &ch = channels.getByIndex(i);
@@ -1181,7 +1204,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_CHANNEL_DETAIL: {
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_CHANNELS));
+ previousPage = MenuPage::NODE_CONFIG_CHANNELS;
+ items.push_back(MenuItem("Back", previousPage));
meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
@@ -1226,7 +1250,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_CHANNEL_PRECISION: {
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_CHANNEL_DETAIL));
+ previousPage = MenuPage::NODE_CONFIG_CHANNEL_DETAIL;
+ items.push_back(MenuItem("Back", previousPage));
meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
if (!ch.settings.has_module_settings || ch.settings.module_settings.position_precision == 0) {
items.push_back(MenuItem("Position is Off", MenuPage::NODE_CONFIG_CHANNEL_DETAIL));
@@ -1247,7 +1272,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case NODE_CONFIG_DEVICE_ROLE: {
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_DEVICE));
+ previousPage = MenuPage::NODE_CONFIG_DEVICE;
+ items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("Client", MenuAction::SET_ROLE_CLIENT, MenuPage::EXIT));
items.push_back(MenuItem("Client Mute", MenuAction::SET_ROLE_CLIENT_MUTE, MenuPage::EXIT));
items.push_back(MenuItem("Router", MenuAction::SET_ROLE_ROUTER, MenuPage::EXIT));
@@ -1257,7 +1283,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
case TIMEZONE:
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_DEVICE));
+ previousPage = MenuPage::NODE_CONFIG_DEVICE;
+ items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("US/Hawaii", SET_TZ_US_HAWAII, MenuPage::NODE_CONFIG_DEVICE));
items.push_back(MenuItem("US/Alaska", SET_TZ_US_ALASKA, MenuPage::NODE_CONFIG_DEVICE));
items.push_back(MenuItem("US/Pacific", SET_TZ_US_PACIFIC, MenuPage::NODE_CONFIG_DEVICE));
@@ -1279,7 +1306,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
break;
case REGION:
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_LORA));
+ previousPage = MenuPage::NODE_CONFIG_LORA;
+ items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("US", MenuAction::SET_REGION_US, MenuPage::EXIT));
items.push_back(MenuItem("EU 868", MenuAction::SET_REGION_EU_868, MenuPage::EXIT));
items.push_back(MenuItem("EU 433", MenuAction::SET_REGION_EU_433, MenuPage::EXIT));
@@ -1310,7 +1338,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
break;
case NODE_CONFIG_PRESET: {
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_LORA));
+ previousPage = MenuPage::NODE_CONFIG_LORA;
+ items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("Long Moderate", MenuAction::SET_PRESET_LONG_MODERATE, MenuPage::EXIT));
items.push_back(MenuItem("Long Fast", MenuAction::SET_PRESET_LONG_FAST, MenuPage::EXIT));
items.push_back(MenuItem("Medium Slow", MenuAction::SET_PRESET_MEDIUM_SLOW, MenuPage::EXIT));
@@ -1323,7 +1352,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
}
// Administration Section
case NODE_CONFIG_ADMIN_RESET:
- items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG));
+ previousPage = MenuPage::NODE_CONFIG;
+ items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("Reset All", MenuAction::RESET_NODEDB_ALL, MenuPage::EXIT));
items.push_back(MenuItem("Keep Favorites Only", MenuAction::RESET_NODEDB_KEEP_FAVORITES, MenuPage::EXIT));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
@@ -1361,8 +1391,14 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
currentPage = page;
}
-void InkHUD::MenuApplet::onRender()
+void InkHUD::MenuApplet::onRender(bool full)
{
+ // Free text mode draws a text input field and skips the normal rendering
+ if (freeTextMode) {
+ drawInputField(0, fontSmall.lineHeight(), X(1.0), Y(1.0) - fontSmall.lineHeight() - 1, cm.freeTextItem.rawText);
+ return;
+ }
+
if (items.size() == 0)
LOG_ERROR("Empty Menu");
@@ -1481,44 +1517,48 @@ void InkHUD::MenuApplet::onRender()
void InkHUD::MenuApplet::onButtonShortPress()
{
- // Push the auto-close timer back
- OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
+ if (!freeTextMode) {
+ // Push the auto-close timer back
+ OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
- if (!settings->joystick.enabled) {
- if (!cursorShown) {
- cursorShown = true;
- cursor = 0;
- } else {
- do {
- cursor = (cursor + 1) % items.size();
- } while (items.at(cursor).isHeader);
- }
- requestUpdate(Drivers::EInk::UpdateTypes::FAST);
- } else {
- if (cursorShown)
- execute(items.at(cursor));
- else
- showPage(MenuPage::EXIT);
- if (!wantsToRender())
+ if (!settings->joystick.enabled) {
+ if (!cursorShown) {
+ cursorShown = true;
+ cursor = 0;
+ } else {
+ do {
+ cursor = (cursor + 1) % items.size();
+ } while (items.at(cursor).isHeader);
+ }
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+ } else {
+ if (cursorShown)
+ execute(items.at(cursor));
+ else
+ showPage(MenuPage::EXIT);
+ if (!wantsToRender())
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+ }
}
}
void InkHUD::MenuApplet::onButtonLongPress()
{
- // Push the auto-close timer back
- OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
+ if (!freeTextMode) {
+ // Push the auto-close timer back
+ OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
- if (cursorShown)
- execute(items.at(cursor));
- else
- showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close
+ if (cursorShown)
+ execute(items.at(cursor));
+ else
+ showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close
- // If we didn't already request a specialized update, when handling a menu action,
- // then perform the usual fast update.
- // FAST keeps things responsive: important because we're dealing with user input
- if (!wantsToRender())
- requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+ // If we didn't already request a specialized update, when handling a menu action,
+ // then perform the usual fast update.
+ // FAST keeps things responsive: important because we're dealing with user input
+ if (!wantsToRender())
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+ }
}
void InkHUD::MenuApplet::onExitShort()
@@ -1531,56 +1571,107 @@ void InkHUD::MenuApplet::onExitShort()
void InkHUD::MenuApplet::onNavUp()
{
- OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
+ if (!freeTextMode) {
+ OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
- if (!cursorShown) {
- cursorShown = true;
- cursor = 0;
- } else {
- do {
- if (cursor == 0)
- cursor = items.size() - 1;
- else
- cursor--;
- } while (items.at(cursor).isHeader);
+ if (!cursorShown) {
+ cursorShown = true;
+ cursor = 0;
+ } else {
+ do {
+ if (cursor == 0)
+ cursor = items.size() - 1;
+ else
+ cursor--;
+ } while (items.at(cursor).isHeader);
+ }
+
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
-
- requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onNavDown()
{
- OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
+ if (!freeTextMode) {
+ OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
- if (!cursorShown) {
- cursorShown = true;
- cursor = 0;
- } else {
- do {
- cursor = (cursor + 1) % items.size();
- } while (items.at(cursor).isHeader);
+ if (!cursorShown) {
+ cursorShown = true;
+ cursor = 0;
+ } else {
+ do {
+ cursor = (cursor + 1) % items.size();
+ } while (items.at(cursor).isHeader);
+ }
+
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
-
- requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
void InkHUD::MenuApplet::onNavLeft()
{
- OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
+ if (!freeTextMode) {
+ OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
- // Go to the previous menu page
- showPage(previousPage);
- requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+ // Go to the previous menu page
+ showPage(previousPage);
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+ }
}
void InkHUD::MenuApplet::onNavRight()
{
- OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
+ if (!freeTextMode) {
+ OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
+ if (cursorShown)
+ execute(items.at(cursor));
+ if (!wantsToRender())
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+ }
+}
- if (cursorShown)
- execute(items.at(cursor));
- if (!wantsToRender())
- requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+void InkHUD::MenuApplet::onFreeText(char c)
+{
+ if (cm.freeTextItem.rawText.length() >= menuTextLimit && c != '\b')
+ return;
+ if (c == '\b') {
+ if (!cm.freeTextItem.rawText.empty())
+ cm.freeTextItem.rawText.pop_back();
+ } else {
+ cm.freeTextItem.rawText += c;
+ }
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+}
+
+void InkHUD::MenuApplet::onFreeTextDone()
+{
+ // Restart the auto-close timeout
+ OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
+ OSThread::enabled = true;
+
+ handleFreeText = false;
+ freeTextMode = false;
+
+ if (!cm.freeTextItem.rawText.empty()) {
+ cm.selectedMessageItem = &cm.freeTextItem;
+ showPage(MenuPage::CANNEDMESSAGE_RECIPIENT);
+ }
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+}
+
+void InkHUD::MenuApplet::onFreeTextCancel()
+{
+ // Restart the auto-close timeout
+ OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
+ OSThread::enabled = true;
+
+ handleFreeText = false;
+ freeTextMode = false;
+
+ // Clear the free text message
+ cm.freeTextItem.rawText.erase();
+
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
// Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu
@@ -1635,6 +1726,10 @@ void InkHUD::MenuApplet::populateSendPage()
// Position / NodeInfo packet
items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT));
+ // If joystick is available, include the Free Text option
+ if (settings->joystick.enabled)
+ items.push_back(MenuItem("Free Text", MenuAction::FREE_TEXT, MenuPage::SEND));
+
// One menu item for each canned message
uint8_t count = cm.store->size();
for (uint8_t i = 0; i < count; i++) {
@@ -1734,6 +1829,48 @@ void InkHUD::MenuApplet::populateRecipientPage()
items.push_back(MenuItem("Exit", MenuPage::EXIT));
}
+void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, std::string text)
+{
+ setFont(fontSmall);
+ uint16_t wrapMaxH = 0;
+
+ // Draw the text, input box, and cursor
+ // Adjusting the box for screen height
+ while (wrapMaxH < height - fontSmall.lineHeight()) {
+ wrapMaxH += fontSmall.lineHeight();
+ }
+
+ // If the text is so long that it goes outside of the input box, the text is actually rendered off screen.
+ uint32_t textHeight = getWrappedTextHeight(0, width - 5, text);
+ if (!text.empty()) {
+ uint16_t textPadding = X(1.0) > Y(1.0) ? wrapMaxH - textHeight : wrapMaxH - textHeight + 1;
+ if (textHeight > wrapMaxH)
+ printWrapped(2, textPadding, width - 5, text);
+ else
+ printWrapped(2, top + 2, width - 5, text);
+ }
+
+ uint16_t textCursorX = text.empty() ? 1 : getCursorX();
+ uint16_t textCursorY = text.empty() ? fontSmall.lineHeight() + 2 : getCursorY() - fontSmall.lineHeight() + 3;
+
+ if (textCursorX + 1 > width - 5) {
+ textCursorX = getCursorX() - width + 5;
+ textCursorY += fontSmall.lineHeight();
+ }
+
+ fillRect(textCursorX + 1, textCursorY, 1, fontSmall.lineHeight(), BLACK);
+
+ // A white rectangle clears the top part of the screen for any text that's printed beyond the input box
+ fillRect(0, 0, X(1.0), top, WHITE);
+
+ // Draw character limit
+ std::string ftlen = std::to_string(text.length()) + "/" + to_string(menuTextLimit);
+ uint16_t textLen = getTextWidth(ftlen);
+ printAt(X(1.0) - textLen - 2, 0, ftlen);
+
+ // Draw the border
+ drawRect(0, top, width, wrapMaxH + 5, BLACK);
+}
// Renders the panel shown at the top of the root menu.
// Displays the clock, and several other pieces of instantaneous system info,
// which we'd prefer not to have displayed in a normal applet, as they update too frequently.
@@ -1875,4 +2012,4 @@ void InkHUD::MenuApplet::freeCannedMessageResources()
cm.messageItems.clear();
cm.recipientItems.clear();
}
-#endif // MESHTASTIC_INCLUDE_INKHUD
\ No newline at end of file
+#endif // MESHTASTIC_INCLUDE_INKHUD
diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h
index 82ccc8f45..7b092153b 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h
+++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h
@@ -32,7 +32,10 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
void onNavDown() override;
void onNavLeft() override;
void onNavRight() override;
- void onRender() override;
+ void onFreeText(char c) override;
+ void onFreeTextDone() override;
+ void onFreeTextCancel() override;
+ void onRender(bool full) override;
void show(Tile *t); // Open the menu, onto a user tile
void setStartPage(MenuPage page);
@@ -51,6 +54,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
+ void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height,
+ std::string text); // Draw input field for free text
uint16_t getSystemInfoPanelHeight();
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
uint16_t *height = nullptr); // Info panel at top of root menu
@@ -62,8 +67,9 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
MenuPage previousPage = MenuPage::EXIT;
uint8_t cursor = 0; // Which menu item is currently highlighted
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
-
+ bool freeTextMode = false;
uint16_t systemInfoPanelHeight = 0; // Need to know before we render
+ uint16_t menuTextLimit = 200;
std::vector