Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions src/ApplicationManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "LEDManager.h"
#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
extern LEDManager ledManager;
#else
#include "platform/NativePlatform.h"
Expand Down Expand Up @@ -193,6 +194,18 @@ void ApplicationManager::handleMessage(const AppMessage& msg) {
case AppMessageType::PRINTER_WARNING:
handlePrinterWarning(msg);
break;

case AppMessageType::KEYPAD_DIGIT:
handleKeypadDigit(msg);
break;

case AppMessageType::KEYPAD_CONFIRM:
handleKeypadConfirm();
break;

case AppMessageType::KEYPAD_CANCEL:
handleKeypadCancel();
break;
}
}

Expand Down Expand Up @@ -905,3 +918,112 @@ void ApplicationManager::publishToHA(const char* topicSuffix, const char* payloa
(void)retained;
#endif
}

// ── Keypad handlers ─────────────────────────────────────────────────────────

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

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;
}

void ApplicationManager::handleKeypadCancel() {
Serial.println("EVENT: KeypadCancel");
keypadBuffer_[0] = '\0';
keypadBufferLen_ = 0;

if (lcdManager) {
lcdManager->updateScreen("Tool entry", "Cleared");
}
}

bool ApplicationManager::sendAssignSpool(const char* toolNumber) {
#ifndef NATIVE_TEST
const char* moonrakerUrl = ConfigurationManager::getInstance().getMoonrakerURL();
if (!moonrakerUrl || moonrakerUrl[0] == '\0') {
Serial.println("ApplicationManager: Moonraker URL not configured — cannot assign spool");
if (lcdManager) lcdManager->updateScreen("Moonraker URL", "Not configured");
return false;
}

extern SemaphoreHandle_t g_httpMutex;
if (g_httpMutex && xSemaphoreTake(g_httpMutex, pdMS_TO_TICKS(5000)) != pdTRUE) {
Serial.println("ApplicationManager: Could not acquire HTTP mutex for ASSIGN_SPOOL");
if (lcdManager) lcdManager->updateScreen("Assign failed", "HTTP busy");
return false;
}

char url[192];
snprintf(url, sizeof(url), "%s/printer/gcode/script", moonrakerUrl);

char gcode[64];
snprintf(gcode, sizeof(gcode), "ASSIGN_SPOOL TOOL=T%s", toolNumber);

char postBody[96];
snprintf(postBody, sizeof(postBody), "{\"script\":\"%s\"}", gcode);

WiFiClient client;
HTTPClient http;
http.setConnectTimeout(3000);
http.setTimeout(5000);
http.begin(client, url);
http.addHeader("Content-Type", "application/json");
int code = http.POST(postBody);
http.end();

if (g_httpMutex) xSemaphoreGive(g_httpMutex);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Serial.printf("ApplicationManager: ASSIGN_SPOOL T%s — HTTP %d\n", toolNumber, code);

if (code != 200) {
if (lcdManager) lcdManager->updateScreen("Assign failed", "Check Moonraker");
return false;
}
return true;
#else
(void)toolNumber;
return true;
#endif
}
18 changes: 11 additions & 7 deletions src/ApplicationManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,10 @@ enum class AppMessageType {
HA_UPDATE_REMAINING, // HA commands remaining weight update
PRINTER_WARNING, // Filament mismatch, temp out of range, etc.

#if ENABLE_KEYPAD
// These events are generated by the keypad/input layer and consumed
// by ApplicationManager to support scan-to-tool assignment workflows.
// Generated by InputManager when keypad is enabled (keypad_on NVS flag)
KEYPAD_DIGIT,
KEYPAD_CONFIRM,
KEYPAD_CANCEL,
#endif
};

enum class AutomationMode : uint8_t {
Expand Down Expand Up @@ -144,12 +141,9 @@ struct AppMessage {
HAWriteTagPayload haWriteTag;
HAUpdateRemainingPayload haUpdateRemaining;
PrinterWarningPayload printerWarning;
#if ENABLE_KEYPAD
// Used when AppMessageType is KEYPAD_DIGIT
struct {
char digit; // '0'–'9'
} keypadDigit;
#endif
} payload;
};

Expand Down Expand Up @@ -187,6 +181,8 @@ class ApplicationManager {
delayedDisplayMaterialName[0] = '\0';
delayedDisplayKgRemaining = 0.0f;
automationMode = AutomationMode::SELF_DIRECTED;
keypadBuffer_[0] = '\0';
keypadBufferLen_ = 0;
}
#endif

Expand Down Expand Up @@ -220,6 +216,10 @@ class ApplicationManager {
// Automation mode
AutomationMode automationMode = AutomationMode::SELF_DIRECTED;

// Keypad tool assignment state
char keypadBuffer_[8] = {0}; // Accumulates typed digits (e.g. "12")
uint8_t keypadBufferLen_ = 0;

// Handlers
void handlePrintStarted(const AppMessage& msg);
void handlePrintEnded(const AppMessage& msg);
Expand All @@ -232,6 +232,10 @@ class ApplicationManager {
void handleHAWriteTag(const AppMessage& msg);
void handleHAUpdateRemaining(const AppMessage& msg);
void handlePrinterWarning(const AppMessage& msg);
void handleKeypadDigit(const AppMessage& msg);
void handleKeypadConfirm();
void handleKeypadCancel();
bool sendAssignSpool(const char* toolNumber);
void finishPrint(float gramsUsed, bool canceled);
void enqueueSpoolmanSync(const SpoolDetectedPayload& spool);
void publishToHA(const char* topicSuffix, const char* payload, bool retained);
Expand Down
22 changes: 21 additions & 1 deletion src/ConfigHTML.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral(
</div>
</section>

<section>
<h2 class="section-title">Klipper / Moonraker</h2>
<div class="hint" style="margin-bottom:12px">Connect to a Klipper printer via Moonraker for keypad-based tool assignment (ASSIGN_SPOOL). Leave blank if not used.</div>
<div class="field">
<label for="moonraker_url">Moonraker URL</label>
<input id="moonraker_url" type="text" maxlength="127" placeholder="http://printer.local:7125" />
</div>
</section>
Comment thread
coderabbitai[bot] marked this conversation as resolved.

<section>
<h2 class="section-title">PrusaLink</h2>
<div class="hint" style="margin-bottom:12px">Connect to a Prusa printer for automatic filament tracking. Get the API key from your printer's web interface.</div>
Expand Down Expand Up @@ -155,7 +164,7 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral(

<section>
<h2 class="section-title">Hardware</h2>
<div class="hint" style="margin-bottom:12px">LCD and LED settings are stored but require matching compile flags to take effect.</div>
<div class="hint" style="margin-bottom:12px">Enable or disable optional hardware peripherals. Changes take effect after reboot.</div>
<div style="display:grid;gap:10px">
<div class="toggle-row">
<span class="toggle-label">LCD Display</span>
Expand All @@ -171,6 +180,13 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral(
<span class="toggle-track"></span>
</label>
</div>
<div class="toggle-row">
<span class="toggle-label">3x4 Matrix Keypad</span>
<label class="toggle-switch">
<input type="checkbox" id="keypad_enabled" />
<span class="toggle-track"></span>
</label>
</div>
</div>
</section>

Expand Down Expand Up @@ -209,6 +225,8 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral(
document.getElementById('prusalink_fields').style.display = cfg.prusalink_on ? '' : 'none';
document.getElementById('lcd_enabled').checked = !!cfg.lcd_enabled;
document.getElementById('led_enabled').checked = !!cfg.led_enabled;
document.getElementById('keypad_enabled').checked = !!cfg.keypad_enabled;
maybeSetValue('moonraker_url', cfg.moonraker_url);
// Password placeholders
if (cfg.wifi_pass_set) document.getElementById('wifi_pass').placeholder = '(set) Leave blank to keep';
if (cfg.mqtt_pass_set) document.getElementById('mqtt_pass').placeholder = '(set) Leave blank to keep';
Expand Down Expand Up @@ -243,6 +261,8 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral(
auto_mode: parseInt(document.getElementById('auto_mode').value) || 0,
lcd_enabled: document.getElementById('lcd_enabled').checked ? 1 : 0,
led_enabled: document.getElementById('led_enabled').checked ? 1 : 0,
keypad_enabled: document.getElementById('keypad_enabled').checked ? 1 : 0,
moonraker_url: document.getElementById('moonraker_url').value.trim(),
prusalink_on: document.getElementById('prusalink_on').checked ? 1 : 0,
prusalink_url: document.getElementById('prusalink_url').value.trim(),
prusalink_api_key: document.getElementById('prusalink_api_key').value
Expand Down
26 changes: 26 additions & 0 deletions src/ConfigurationManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ static const char* NVS_KEY_SPOOLMAN_URL = "spoolman_url";
static const char* NVS_KEY_AUTO_MODE = "auto_mode";
static const char* NVS_KEY_LCD_ON = "lcd_on";
static const char* NVS_KEY_LED_ON = "led_on";
static const char* NVS_KEY_KEYPAD_ON = "keypad_on";
static const char* NVS_KEY_MOONRAKER_URL = "moonraker_url";
static const char* NVS_KEY_PRUSALINK_ON = "prusalink_on";
static const char* NVS_KEY_PRUSALINK_URL = "prusalink_url";
static const char* NVS_KEY_PRUSALINK_KEY = "prusalink_key";
Expand Down Expand Up @@ -91,6 +93,9 @@ void ConfigurationManager::loadFromDeviceConfig() {

_automationMode = cfg.automation_mode;

// Moonraker — not in DeviceConfig, configured via NVS/web UI
_moonrakerUrl[0] = '\0';

// PrusaLink defaults — not in DeviceConfig, disabled by default
_prusaLinkEnabled = false;
_prusaLinkUrl[0] = '\0';
Expand All @@ -99,6 +104,7 @@ void ConfigurationManager::loadFromDeviceConfig() {
// Optional hardware feature defaults from compile-time flags
_lcdEnabled = cfg.peripherals.lcd_enabled;
_ledEnabled = cfg.peripherals.status_led_enabled;
_keypadEnabled = cfg.peripherals.keypad_enabled;
}

#ifndef NATIVE_TEST
Expand Down Expand Up @@ -174,6 +180,14 @@ bool ConfigurationManager::loadFromNVS() {
_ledEnabled = prefs.getUChar(NVS_KEY_LED_ON, _ledEnabled ? 1 : 0) != 0;
anyOverride = true;
}
if (prefs.isKey(NVS_KEY_MOONRAKER_URL)) {
prefs.getString(NVS_KEY_MOONRAKER_URL, _moonrakerUrl, sizeof(_moonrakerUrl));
anyOverride = true;
}
if (prefs.isKey(NVS_KEY_KEYPAD_ON)) {
_keypadEnabled = prefs.getUChar(NVS_KEY_KEYPAD_ON, _keypadEnabled ? 1 : 0) != 0;
anyOverride = true;
}

prefs.end();
return anyOverride;
Expand Down Expand Up @@ -248,6 +262,14 @@ bool ConfigurationManager::isLedEnabled() const {
return _ledEnabled;
}

bool ConfigurationManager::isKeypadEnabled() const {
return _keypadEnabled;
}

const char* ConfigurationManager::getMoonrakerURL() const {
return _moonrakerUrl;
}

void ConfigurationManager::getCurrentConfig(ConfigUpdate& out) const {
memset(&out, 0, sizeof(out));
strncpy(out.wifi_ssid, _ssid, sizeof(out.wifi_ssid) - 1);
Expand All @@ -264,6 +286,8 @@ void ConfigurationManager::getCurrentConfig(ConfigUpdate& out) const {
strncpy(out.prusalink_api_key, _prusaLinkApiKey, sizeof(out.prusalink_api_key) - 1);
out.lcd_enabled = _lcdEnabled ? 1 : 0;
out.led_enabled = _ledEnabled ? 1 : 0;
out.keypad_enabled = _keypadEnabled ? 1 : 0;
strncpy(out.moonraker_url, _moonrakerUrl, sizeof(out.moonraker_url) - 1);
}

#ifndef NATIVE_TEST
Expand All @@ -290,6 +314,8 @@ bool ConfigurationManager::saveToNVS(const ConfigUpdate& update) {
prefs.putUChar(NVS_KEY_AUTO_MODE, update.auto_mode);
prefs.putUChar(NVS_KEY_LCD_ON, update.lcd_enabled);
prefs.putUChar(NVS_KEY_LED_ON, update.led_enabled);
prefs.putUChar(NVS_KEY_KEYPAD_ON, update.keypad_enabled);
prefs.putString(NVS_KEY_MOONRAKER_URL, update.moonraker_url);
prefs.putBool(NVS_KEY_PRUSALINK_ON, update.prusalink_on != 0);
prefs.putString(NVS_KEY_PRUSALINK_URL, update.prusalink_url);
if (update.prusalink_api_key[0] != '\0') {
Expand Down
11 changes: 11 additions & 0 deletions src/ConfigurationManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ struct ConfigUpdate {
uint8_t auto_mode;
uint8_t lcd_enabled;
uint8_t led_enabled;
uint8_t keypad_enabled;
// Klipper / Moonraker
char moonraker_url[128];
// PrusaLink integration
uint8_t prusalink_on;
char prusalink_url[128];
Expand Down Expand Up @@ -47,6 +50,9 @@ class ConfigurationManager {
const char* getHAMqttPass() const;
uint8_t getAutomationMode() const;

// Klipper / Moonraker
const char* getMoonrakerURL() const;

// PrusaLink configuration
bool isPrusaLinkEnabled() const;
const char* getPrusaLinkURL() const;
Expand All @@ -55,6 +61,7 @@ class ConfigurationManager {
// Optional hardware features (compile-time default, overridable via NVS)
bool isLcdEnabled() const;
bool isLedEnabled() const;
bool isKeypadEnabled() const;

// Web config support
void getCurrentConfig(ConfigUpdate& out) const;
Expand Down Expand Up @@ -84,6 +91,9 @@ class ConfigurationManager {
char _haMqttPass[64];
uint8_t _automationMode;

// Klipper / Moonraker
char _moonrakerUrl[128] = {0};

// PrusaLink config
bool _prusaLinkEnabled = false;
char _prusaLinkUrl[128] = {0};
Expand All @@ -92,6 +102,7 @@ class ConfigurationManager {
// Optional hardware features
bool _lcdEnabled = false;
bool _ledEnabled = false;
bool _keypadEnabled = false;

bool _initialized = false;
};
Expand Down
4 changes: 4 additions & 0 deletions src/DeviceConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
#ifndef AUTOMATION_MODE
#define AUTOMATION_MODE 0
#endif
#ifndef ENABLE_KEYPAD
#define ENABLE_KEYPAD 0
#endif

static const DeviceConfig kDeviceConfig = {
.device_name = DEVICE_NAME,
Expand Down Expand Up @@ -39,6 +42,7 @@ static const DeviceConfig kDeviceConfig = {
.peripherals = {
.lcd_enabled = ENABLE_LCD,
.status_led_enabled = ENABLE_STATUS_LED,
.keypad_enabled = ENABLE_KEYPAD,
},

.automation_mode = AUTOMATION_MODE,
Expand Down
Loading