Skip to content

feat: 3x4 matrix keypad support with Moonraker ASSIGN_SPOOL integration#46

Merged
sjordan0228 merged 2 commits into
devfrom
feature/keypad-support
Mar 27, 2026
Merged

feat: 3x4 matrix keypad support with Moonraker ASSIGN_SPOOL integration#46
sjordan0228 merged 2 commits into
devfrom
feature/keypad-support

Conversation

@sjordan0228
Copy link
Copy Markdown
Contributor

@sjordan0228 sjordan0228 commented Mar 27, 2026

Summary

  • InputManager — new 3x4 matrix keypad driver. Polls for key presses, sends KEYPAD_DIGIT/CONFIRM/CANCEL messages to ApplicationManager. Initialized conditionally via isKeypadEnabled() NVS flag.
  • Keypad workflow — scan a spool, type a tool number (multi-digit supported, e.g. 12), press # to confirm. Scanner POSTs ASSIGN_SPOOL TOOL=T{n} directly to Moonraker. * clears entered digits. LCD shows Assign to: T{n} as digits are typed.
  • Moonraker URL config — new moonraker_url field in ConfigurationManager, NVS, web config UI, and installer. Full URL format (e.g. http://192.168.1.72:7125).
  • NVS runtime model#if ENABLE_KEYPAD compile-time guards removed from ApplicationManager.h. Keypad message types always compiled in; hardware init gated by NVS keypad_on flag at runtime, matching the LCD/LED pattern.
  • Config web UI — new Klipper/Moonraker section with URL field, keypad toggle in Hardware section, updated hint text.

Addresses #34.

Test plan

  • Config page shows Klipper/Moonraker URL field and Keypad toggle
  • Moonraker URL persists across reboot
  • Keypad disabled by default — InputManager does not initialize
  • (Hardware needed) Scan tag, type tool number, press # — LCD shows assignment, Moonraker receives ASSIGN_SPOOL
  • (Hardware needed) Press * to clear digits before confirming
  • (Hardware needed) Press # with no spool scanned — shows "No spool scanned"
  • (Hardware needed) Press # with no digits entered — shows "No tool entered"
  • Compile clean on both esp32dev and esp32s3zero

Summary by CodeRabbit

  • New Features
    • 3x4 matrix keypad support for entering tool numbers with confirm/cancel controls and on-screen prompts.
    • One-touch spool assignment from the device (with success/error messages shown on the display).
    • Integration with Moonraker/Klipper: configure a Moonraker URL to enable networked spool assignment.
    • Web UI settings to enable/disable the keypad and to set the Moonraker server URL.

- InputManager: polls 3x4 keypad, sends KEYPAD_DIGIT/CONFIRM/CANCEL
  messages to ApplicationManager
- ApplicationManager: accumulates digits, shows "Assign to: T{n}" on LCD,
  POSTs ASSIGN_SPOOL TOOL=T{n} to Moonraker on # confirm, * clears
- ConfigurationManager: moonraker_url and keypad_on NVS fields with
  runtime accessors (isKeypadEnabled, getMoonrakerURL)
- DeviceConfig: keypad_enabled added to PeripheralConfig
- Config web UI: Klipper/Moonraker URL section, keypad toggle in Hardware,
  updated hint text for NVS-based hardware control
- WebServerManager: moonraker_url and keypad_enabled in GET/POST config API
- ApplicationManager.h: removed #if ENABLE_KEYPAD compile-time guards,
  keypad message types always compiled in (NVS runtime model)
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 27, 2026

📝 Walkthrough

Walkthrough

Adds optional 3x4 keypad input, message handlers, and UI/config support to assign spools via Moonraker: keypad events feed ApplicationManager, which validates state (NFC when present) and posts ASSIGN_SPOOL G-code to Moonraker; keypad enablement and Moonraker URL are persisted and exposed in the web UI/API.

Changes

Cohort / File(s) Summary
Keypad Input Manager
src/InputManager.h, src/InputManager.cpp
New singleton that reads a 3x4 matrix keypad (50ms debounce), maps keys to AppMessage events: digits → KEYPAD_DIGIT, #KEYPAD_CONFIRM, *KEYPAD_CANCEL, and forwards messages to ApplicationManager.
Application logic & HTTP integration
src/ApplicationManager.h, src/ApplicationManager.cpp
Added keypad state (keypadBuffer_, keypadBufferLen_) and handlers (handleKeypadDigit, handleKeypadConfirm, handleKeypadCancel). Implemented sendAssignSpool(const char*) which POSTs {"script":"ASSIGN_SPOOL TOOL=T<tool>"} to <moonrakerUrl>/printer/gcode/script (non-native builds), using g_httpMutex with 5s timeout; LCD updates and NFC presence checks integrated.
Configuration model & persistence
src/ConfigurationManager.h, src/ConfigurationManager.cpp, src/DeviceConfig.h, src/DeviceConfig.cpp
Added _moonrakerUrl[128] and _keypadEnabled cache fields, NVS keys for moonraker_url and keypad_on handling, ConfigUpdate gains moonraker_url and keypad_enabled, new accessors getMoonrakerURL() and isKeypadEnabled(), and load/save extended to persist these fields. DeviceConfig gains peripherals.keypad_enabled default from ENABLE_KEYPAD.
Web UI & API
src/ConfigHTML.h, src/WebServerManager.cpp
Config HTML adds “Klipper / Moonraker” moonraker_url input and a “3x4 Matrix Keypad” keypad_enabled toggle. API now includes/accepts moonraker_url and keypad_enabled; JSON parsing buffer increased to StaticJsonDocument<1024>.
Integration in lifecycle
src/main.cpp
Calls InputManager::begin() during setup and InputManager::poll() during loop when ConfigurationManager::isKeypadEnabled() is true.
Device messaging types
src/ApplicationManager.h
Keypad-related AppMessageType variants and payload field made always available (no longer conditionally compiled); resetForTest() initializes keypad state under NATIVE_TEST.

Sequence Diagram(s)

sequenceDiagram
    participant Keypad as Matrix Keypad
    participant InputMgr as InputManager
    participant AppMgr as ApplicationManager
    participant NFC as NFCManager
    participant LCD as LCD
    participant Moonraker as Moonraker API

    Keypad->>InputMgr: key '5' pressed
    InputMgr->>AppMgr: sendMessage(KEYPAD_DIGIT, '5')
    AppMgr->>AppMgr: handleKeypadDigit('5') -> append buffer
    AppMgr->>LCD: update "Assign to: T5"

    Keypad->>InputMgr: '#' pressed (confirm)
    InputMgr->>AppMgr: sendMessage(KEYPAD_CONFIRM)
    AppMgr->>AppMgr: handleKeypadConfirm()
    AppMgr->>AppMgr: if buffer empty -> LCD error
    AppMgr->>NFC: check spool present (non-native)
    alt Spool present or native
        AppMgr->>AppMgr: sendAssignSpool("5")
        AppMgr->>AppMgr: acquire g_httpMutex (5s)
        AppMgr->>Moonraker: POST /printer/gcode/script {"script":"ASSIGN_SPOOL TOOL=T5"}
        Moonraker-->>AppMgr: 200 OK
        AppMgr->>LCD: display "Assigned"
        AppMgr->>AppMgr: clear buffer
    else No spool
        AppMgr->>LCD: display error, clear buffer
    end

    Keypad->>InputMgr: '*' pressed (cancel)
    InputMgr->>AppMgr: sendMessage(KEYPAD_CANCEL)
    AppMgr->>AppMgr: handleKeypadCancel() -> clear buffer
    AppMgr->>LCD: display "Cleared"
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding 3x4 matrix keypad support and integrating it with Moonraker's ASSIGN_SPOOL command.
Description check ✅ Passed The description covers all required template sections: Summary, Changes, How to Test, and Checklist. It provides clear implementation details and comprehensive test scenarios.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/keypad-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ApplicationManager.cpp`:
- Around line 924-960: The keypad buffer is not tied to a specific scanned spool
so entries can be applied to the wrong tag; fix by binding entry to the current
spool when typing starts and validating on confirm: in handleKeypadDigit, when
keypadBufferLen_ == 0 capture the current spool ID via
NFCManager::getInstance().getCurrentSpoolState(...) into a new member (e.g.
currentSpoolId_) and set a flag (e.g. keypadBoundSpool_), and if no spool
present refuse to start entry; in handleKeypadConfirm, call
NFCManager::getInstance().getCurrentSpoolState(...) again and compare the
current spool ID to currentSpoolId_ and if they differ clear
keypadBuffer_/keypadBufferLen_, reset keypadBoundSpool_, update the LCD with a
"spool changed" message and abort; also ensure any NFC spool-state change
handler (where CurrentSpoolState is processed) clears
keypadBuffer_/keypadBufferLen_ and keypadBoundSpool_ when the tag is removed or
a different tag is detected so the buffer cannot be reused for another spool
before confirm, then call sendAssignSpool only when the spool IDs match.
- Around line 960-969: sendAssignSpool currently may return early on error but
the caller always shows "Assigned..." and clears the entry; change
sendAssignSpool to return a status (bool or enum) indicating success or failure,
then update the caller in ApplicationManager.cpp to check that return value
before updating lcdManager->updateScreen(...) and before clearing keypadBuffer_
and keypadBufferLen_; use the symbols sendAssignSpool, lcdManager,
keypadBuffer_, and keypadBufferLen_ to locate the code and ensure only on
success you show the Assigned message and clear the buffer, otherwise leave the
buffer intact and show an error/failure message.
- Around line 991-1013: handleKeypadConfirm() performs a blocking HTTP POST
inside ApplicationManager::processMessages() which can freeze the event loop;
add explicit HTTPClient timeouts (call http.setConnectTimeout(...) and
http.setTimeout(...) on the HTTPClient instance before http.begin()/http.POST())
to bound connect/read waits, and then consider moving the POST logic into a
dedicated task (similar to otaDownloadTask in WebServerManager) that performs
the HTTP work and posts the result back to the main loop via an AppMessage;
update references in ApplicationManager (handleKeypadConfirm(), g_httpMutex
usage) to use the new task or at minimum ensure timeouts are set before calling
http.POST().

In `@src/ConfigHTML.h`:
- Around line 123-130: The moonraker_url text input lacks a maxlength and can
accept strings longer than the server-side buffer ConfigUpdate::moonraker_url
(128 bytes), causing silent truncation; update the input element with
id="moonraker_url" to include maxlength="127" (client-side enforcement) and
ensure the placeholder/label remain unchanged so users get immediate feedback
when typing long URLs.

In `@src/WebServerManager.cpp`:
- Line 503: The JSON buffer for parsing is likely undersized: increase the
StaticJsonDocument capacity used in the parsing code (the
StaticJsonDocument<768> instance named doc) to a larger size (e.g.,
StaticJsonDocument<1024>) so deserializeJson has enough headroom for
ConfigUpdate’s string fields and JSON overhead; update any related comments and
re-run deserializeJson error checks to ensure failures no longer occur when
ConfigUpdate fields approach their max lengths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 01e8df1a-fcb0-4500-aef3-62e226649e75

📥 Commits

Reviewing files that changed from the base of the PR and between 0f38ae9 and e2ebee5.

📒 Files selected for processing (11)
  • src/ApplicationManager.cpp
  • src/ApplicationManager.h
  • src/ConfigHTML.h
  • src/ConfigurationManager.cpp
  • src/ConfigurationManager.h
  • src/DeviceConfig.cpp
  • src/DeviceConfig.h
  • src/InputManager.cpp
  • src/InputManager.h
  • src/WebServerManager.cpp
  • src/main.cpp

Comment thread src/ApplicationManager.cpp Outdated
Comment on lines +924 to +960
void ApplicationManager::handleKeypadDigit(const AppMessage& msg) {
char digit = msg.payload.keypadDigit.digit;
Serial.printf("EVENT: KeypadDigit - '%c'\n", digit);

if (keypadBufferLen_ < sizeof(keypadBuffer_) - 1) {
keypadBuffer_[keypadBufferLen_++] = digit;
keypadBuffer_[keypadBufferLen_] = '\0';
}

if (lcdManager) {
char line[17];
snprintf(line, sizeof(line), "Assign to: T%s", keypadBuffer_);
lcdManager->updateScreen(line, "# Confirm * Clr");
}
}

void ApplicationManager::handleKeypadConfirm() {
Serial.println("EVENT: KeypadConfirm");

if (keypadBufferLen_ == 0) {
if (lcdManager) lcdManager->updateScreen("No tool entered", "Type number + #");
return;
}

#ifndef NATIVE_TEST
// Check if a spool is currently detected
CurrentSpoolState state;
bool spoolPresent = NFCManager::getInstance().getCurrentSpoolState(state) && state.present;
if (!spoolPresent) {
if (lcdManager) lcdManager->updateScreen("No spool scanned", "Scan tag first");
keypadBuffer_[0] = '\0';
keypadBufferLen_ = 0;
return;
}
#endif

sendAssignSpool(keypadBuffer_);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bind the buffered tool entry to the current scan.

This flow only checks that some spool is present when # is pressed. Digits entered before a scan, or for a previous scan, will be applied to whichever tag happens to be present now. Clear the buffer whenever the tag context changes, or capture the current spool ID when entry starts and validate it here before posting.

🧰 Tools
🪛 Clang (14.0.6)

[warning] 933-933: implicit conversion 'LCDManager *' -> bool

(readability-implicit-bool-conversion)


[warning] 934-934: do not declare C-style arrays, use std::array<> instead

(modernize-avoid-c-arrays)


[warning] 944-944: implicit conversion 'LCDManager *' -> bool

(readability-implicit-bool-conversion)


[warning] 944-944: statement should be inside braces

(readability-braces-around-statements)


[warning] 953-953: implicit conversion 'LCDManager *' -> bool

(readability-implicit-bool-conversion)


[warning] 953-953: statement should be inside braces

(readability-braces-around-statements)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ApplicationManager.cpp` around lines 924 - 960, The keypad buffer is not
tied to a specific scanned spool so entries can be applied to the wrong tag; fix
by binding entry to the current spool when typing starts and validating on
confirm: in handleKeypadDigit, when keypadBufferLen_ == 0 capture the current
spool ID via NFCManager::getInstance().getCurrentSpoolState(...) into a new
member (e.g. currentSpoolId_) and set a flag (e.g. keypadBoundSpool_), and if no
spool present refuse to start entry; in handleKeypadConfirm, call
NFCManager::getInstance().getCurrentSpoolState(...) again and compare the
current spool ID to currentSpoolId_ and if they differ clear
keypadBuffer_/keypadBufferLen_, reset keypadBoundSpool_, update the LCD with a
"spool changed" message and abort; also ensure any NFC spool-state change
handler (where CurrentSpoolState is processed) clears
keypadBuffer_/keypadBufferLen_ and keypadBoundSpool_ when the tag is removed or
a different tag is detected so the buffer cannot be reused for another spool
before confirm, then call sendAssignSpool only when the spool IDs match.

Comment thread src/ApplicationManager.cpp Outdated
Comment on lines +960 to +969
sendAssignSpool(keypadBuffer_);

if (lcdManager) {
char line[17];
snprintf(line, sizeof(line), "Assigned T%s", keypadBuffer_);
lcdManager->updateScreen(line, "OK");
}

keypadBuffer_[0] = '\0';
keypadBufferLen_ = 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't overwrite assignment failures with a success screen.

sendAssignSpool() can return early for a missing URL, HTTP-mutex timeout, or non-200 response, but this block still shows Assigned... and clears the entry. That gives a false-positive UI and forces the user to retype the tool number for retry.

Possible fix
-    sendAssignSpool(keypadBuffer_);
-
-    if (lcdManager) {
-        char line[17];
-        snprintf(line, sizeof(line), "Assigned T%s", keypadBuffer_);
-        lcdManager->updateScreen(line, "OK");
-    }
-
-    keypadBuffer_[0] = '\0';
-    keypadBufferLen_ = 0;
+    if (sendAssignSpool(keypadBuffer_)) {
+        if (lcdManager) {
+            char line[17];
+            snprintf(line, sizeof(line), "Assigned T%s", keypadBuffer_);
+            lcdManager->updateScreen(line, "OK");
+        }
+        keypadBuffer_[0] = '\0';
+        keypadBufferLen_ = 0;
+    }

This needs sendAssignSpool() to return a status/result.

🧰 Tools
🪛 Clang (14.0.6)

[warning] 962-962: implicit conversion 'LCDManager *' -> bool

(readability-implicit-bool-conversion)


[warning] 963-963: do not declare C-style arrays, use std::array<> instead

(modernize-avoid-c-arrays)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ApplicationManager.cpp` around lines 960 - 969, sendAssignSpool currently
may return early on error but the caller always shows "Assigned..." and clears
the entry; change sendAssignSpool to return a status (bool or enum) indicating
success or failure, then update the caller in ApplicationManager.cpp to check
that return value before updating lcdManager->updateScreen(...) and before
clearing keypadBuffer_ and keypadBufferLen_; use the symbols sendAssignSpool,
lcdManager, keypadBuffer_, and keypadBufferLen_ to locate the code and ensure
only on success you show the Assigned message and clear the buffer, otherwise
leave the buffer intact and show an error/failure message.

Comment thread src/ApplicationManager.cpp
Comment thread src/ConfigHTML.h
Comment thread src/WebServerManager.cpp Outdated
- sendAssignSpool returns bool; caller only shows "Assigned OK" on success
- HTTP connect timeout (3s) and read timeout (5s) on Moonraker POST
  to prevent blocking the event loop if Moonraker is unreachable
- maxlength="127" on moonraker_url input to match server buffer size
- JSON parse buffer bumped from 768 to 1024 for config POST handler
- Replaced personal IP in placeholder with generic printer.local
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/ApplicationManager.cpp (1)

924-938: ⚠️ Potential issue | 🟠 Major

Keypad buffer not bound to scanned spool — risk of misassignment.

The digit buffer accumulates input without tracking which spool was present when typing started. A user could type digits, swap the NFC tag, then press # — assigning to the wrong spool.

Consider capturing the spool UID when the first digit is entered (when keypadBufferLen_ == 0) and validating it matches at confirm time. Additionally, clear the buffer in handleTagRemoved() so stale input cannot be applied to a different tag.

🛡️ Sketch of proposed fix

Add a member to track the spool being assigned:

// In ApplicationManager.h, add:
char keypadBoundSpoolId_[17] = {0};

Then in handleKeypadDigit:

 void ApplicationManager::handleKeypadDigit(const AppMessage& msg) {
     char digit = msg.payload.keypadDigit.digit;
     Serial.printf("EVENT: KeypadDigit - '%c'\n", digit);
 
+#ifndef NATIVE_TEST
+    // Bind to current spool when first digit is entered
+    if (keypadBufferLen_ == 0) {
+        CurrentSpoolState state;
+        if (NFCManager::getInstance().getCurrentSpoolState(state) && state.present) {
+            strncpy(keypadBoundSpoolId_, state.spool_id, sizeof(keypadBoundSpoolId_) - 1);
+        } else {
+            keypadBoundSpoolId_[0] = '\0';
+        }
+    }
+#endif
+
     if (keypadBufferLen_ < sizeof(keypadBuffer_) - 1) {

And in handleKeypadConfirm, validate the spool matches:

     bool spoolPresent = NFCManager::getInstance().getCurrentSpoolState(state) && state.present;
-    if (!spoolPresent) {
+    if (!spoolPresent || strcmp(state.spool_id, keypadBoundSpoolId_) != 0) {
+        if (lcdManager) lcdManager->updateScreen("Spool changed", "Re-enter tool #");

Clear buffer on tag removal in handleTagRemoved:

 void ApplicationManager::handleTagRemoved(const AppMessage& msg) {
     // ... existing code ...
+    keypadBuffer_[0] = '\0';
+    keypadBufferLen_ = 0;
+    keypadBoundSpoolId_[0] = '\0';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ApplicationManager.cpp` around lines 924 - 938, The keypad buffer must be
bound to the NFC spool UID to prevent cross-tag assignment: add a member (e.g.,
keypadBoundSpoolId_ char[17]) to ApplicationManager, on the first digit input in
handleKeypadDigit (when keypadBufferLen_ == 0) capture the current tag UID into
keypadBoundSpoolId_, and on confirm in handleKeypadConfirm verify the stored
keypadBoundSpoolId_ matches the currently-present tag UID before applying the
assignment (reject/clear if it doesn't match); also clear keypadBuffer_ and
keypadBoundSpoolId_ in handleTagRemoved (and when user presses clear or buffer
is emptied) so stale input cannot be used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/ApplicationManager.cpp`:
- Around line 924-938: The keypad buffer must be bound to the NFC spool UID to
prevent cross-tag assignment: add a member (e.g., keypadBoundSpoolId_ char[17])
to ApplicationManager, on the first digit input in handleKeypadDigit (when
keypadBufferLen_ == 0) capture the current tag UID into keypadBoundSpoolId_, and
on confirm in handleKeypadConfirm verify the stored keypadBoundSpoolId_ matches
the currently-present tag UID before applying the assignment (reject/clear if it
doesn't match); also clear keypadBuffer_ and keypadBoundSpoolId_ in
handleTagRemoved (and when user presses clear or buffer is emptied) so stale
input cannot be used.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 61a3967f-ebbc-41ad-887d-d20e9ea7d36e

📥 Commits

Reviewing files that changed from the base of the PR and between e2ebee5 and fa47481.

📒 Files selected for processing (4)
  • src/ApplicationManager.cpp
  • src/ApplicationManager.h
  • src/ConfigHTML.h
  • src/WebServerManager.cpp

@sjordan0228 sjordan0228 merged commit 9f7ecec into dev Mar 27, 2026
1 check passed
@coderabbitai coderabbitai Bot mentioned this pull request Mar 28, 2026
7 tasks
@sjordan0228 sjordan0228 deleted the feature/keypad-support branch March 28, 2026 23:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant